diff --git a/3rd-dependencies/compile.txt b/3rd-dependencies/compile.txt new file mode 100755 index 000000000..c8c59904e --- /dev/null +++ b/3rd-dependencies/compile.txt @@ -0,0 +1,105 @@ + aopalliance:aopalliance:jar:1.0:compile + com.fasterxml.jackson.core:jackson-annotations:jar:2.5.0:compile + com.fasterxml.jackson.core:jackson-core:jar:2.5.0:compile + com.fasterxml.jackson.core:jackson-databind:jar:2.5.0:compile + com.fasterxml:classmate:jar:1.3.0:compile + com.google.guava:guava:jar:18.0:compile + com.h2database:h2:jar:1.4.186:compile + com.rabbitmq:amqp-client:jar:3.5.5:compile + com.vaadin.external.atmosphere:atmosphere-runtime:jar:2.2.7.vaadin1:compile + com.vaadin.external.flute:flute:jar:1.3.0.gg2:compile + com.vaadin.external.google:guava:jar:16.0.1.vaadin1:compile + com.vaadin.external.slf4j:vaadin-slf4j-jdk14:jar:1.6.1:compile + com.vaadin.external.streamhtmlparser:streamhtmlparser-jsilver:jar:0.0.10.vaadin1:compile + com.vaadin:vaadin-push:jar:7.5.7:compile + com.vaadin:vaadin-sass-compiler:jar:0.9.12:compile + com.vaadin:vaadin-server:jar:7.5.7:compile + com.vaadin:vaadin-shared:jar:7.5.7:compile + com.vaadin:vaadin-spring-boot:jar:1.0.0:compile + com.vaadin:vaadin-spring:jar:1.0.0:compile + com.vaadin:vaadin-themes:jar:7.5.7:compile + commons-io:commons-io:jar:2.4:compile + cz.jirutka.rsql:rsql-parser:jar:2.0.0:compile + io.springfox:springfox-core:jar:2.0.3:compile + io.springfox:springfox-schema:jar:2.0.3:compile + io.springfox:springfox-spi:jar:2.0.3:compile + io.springfox:springfox-spring-web:jar:2.0.3:compile + io.springfox:springfox-swagger-common:jar:2.0.3:compile + io.springfox:springfox-swagger2:jar:2.0.3:compile + io.swagger:swagger-annotations:jar:1.5.0:compile + io.swagger:swagger-models:jar:1.5.0:compile + javax.servlet:javax.servlet-api:jar:3.1.0:compile + javax.transaction:javax.transaction-api:jar:1.2:compile + javax.validation:validation-api:jar:1.1.0.Final:compile + joda-time:joda-time:jar:2.5:compile + net._01001111:jlorem:jar:1.1:compile + org.apache.commons:commons-lang3:jar:3.4:compile + org.apache.commons:commons-pool2:jar:2.2:compile + org.apache.logging.log4j:log4j-api:jar:2.1:compile + org.apache.logging.log4j:log4j-core:jar:2.1:compile + org.apache.logging.log4j:log4j-slf4j-impl:jar:2.1:compile + org.apache.tomcat:tomcat-jdbc:jar:8.0.28:compile + org.apache.tomcat:tomcat-juli:jar:8.0.28:compile + org.aspectj:aspectjrt:jar:1.8.5:compile + org.aspectj:aspectjweaver:jar:1.8.5:compile + org.eclipse.persistence:javax.persistence:jar:2.1.0:compile + org.eclipse.persistence:org.eclipse.persistence.antlr:jar:2.6.0:compile + org.eclipse.persistence:org.eclipse.persistence.asm:jar:2.6.0:compile + org.eclipse.persistence:org.eclipse.persistence.core:jar:2.6.0:compile + org.eclipse.persistence:org.eclipse.persistence.jpa.jpql:jar:2.6.0:compile + org.eclipse.persistence:org.eclipse.persistence.jpa:jar:2.6.0:compile + org.flywaydb:flyway-core:jar:3.1:compile + org.glassfish:javax.json:jar:1.0.4:compile + org.hibernate:hibernate-validator:jar:5.2.2.Final:compile + org.jboss.logging:jboss-logging:jar:3.2.1.Final:compile + org.jsoup:jsoup:jar:1.8.1:compile + org.mapstruct:mapstruct:jar:1.0.0.Beta4:compile + org.mongodb:mongo-java-driver:jar:3.0.2:compile + org.objenesis:objenesis:jar:2.1:compile + org.slf4j:jcl-over-slf4j:jar:1.7.12:compile + org.slf4j:jul-to-slf4j:jar:1.7.12:compile + org.slf4j:slf4j-api:jar:1.7.7:compile + org.springframework.amqp:spring-amqp:jar:1.4.6.RELEASE:compile + org.springframework.amqp:spring-rabbit:jar:1.4.6.RELEASE:compile + org.springframework.boot:spring-boot-autoconfigure:jar:1.2.7.RELEASE:compile + org.springframework.boot:spring-boot-starter-aop:jar:1.2.7.RELEASE:compile + org.springframework.boot:spring-boot-starter-data-jpa:jar:1.2.7.RELEASE:compile + org.springframework.boot:spring-boot-starter-jdbc:jar:1.2.7.RELEASE:compile + org.springframework.boot:spring-boot-starter-log4j2:jar:1.2.7.RELEASE:compile + org.springframework.boot:spring-boot-starter-web:jar:1.2.7.RELEASE:compile + org.springframework.boot:spring-boot-starter:jar:1.2.7.RELEASE:compile + org.springframework.boot:spring-boot:jar:1.2.7.RELEASE:compile + org.springframework.data:spring-data-commons:jar:1.10.1.RELEASE:compile + org.springframework.data:spring-data-jpa:jar:1.8.1.RELEASE:compile + org.springframework.data:spring-data-mongodb:jar:1.7.1.RELEASE:compile + org.springframework.data:spring-data-redis:jar:1.5.1.RELEASE:compile + org.springframework.hateoas:spring-hateoas:jar:0.16.0.RELEASE:compile + org.springframework.plugin:spring-plugin-core:jar:1.1.0.RELEASE:compile + org.springframework.plugin:spring-plugin-metadata:jar:1.2.0.RELEASE:compile + org.springframework.retry:spring-retry:jar:1.1.2.RELEASE:compile + org.springframework.security:spring-security-aspects:jar:3.2.8.RELEASE:compile + org.springframework.security:spring-security-config:jar:3.2.8.RELEASE:compile + org.springframework.security:spring-security-core:jar:3.2.8.RELEASE:compile + org.springframework.security:spring-security-web:jar:3.2.8.RELEASE:compile + org.springframework:spring-aop:jar:4.1.8.RELEASE:compile + org.springframework:spring-aspects:jar:4.1.8.RELEASE:compile + org.springframework:spring-beans:jar:4.1.8.RELEASE:compile + org.springframework:spring-context-support:jar:4.1.8.RELEASE:compile + org.springframework:spring-context:jar:4.1.8.RELEASE:compile + org.springframework:spring-core:jar:4.1.8.RELEASE:compile + org.springframework:spring-expression:jar:4.1.8.RELEASE:compile + org.springframework:spring-jdbc:jar:4.1.8.RELEASE:compile + org.springframework:spring-messaging:jar:4.1.8.RELEASE:compile + org.springframework:spring-orm:jar:4.1.8.RELEASE:compile + org.springframework:spring-tx:jar:4.1.8.RELEASE:compile + org.springframework:spring-web:jar:4.1.8.RELEASE:compile + org.springframework:spring-webmvc:jar:4.1.8.RELEASE:compile + org.vaadin.addons.lazyquerycontainer:vaadin-lazyquerycontainer:jar:7.4.0.1:compile + org.vaadin.addons:flexibleoptiongroup:jar:2.2.0:compile + org.vaadin.addons:tokenfield:jar:7.0.1:compile + org.vaadin.spring.addons:vaadin-spring-addon-eventbus:jar:0.0.6.RELEASE:compile + org.vaadin.spring.extensions:vaadin-spring-ext-core:jar:0.0.6.RELEASE:compile + org.vaadin.spring.extensions:vaadin-spring-ext-security:jar:0.0.6.RELEASE:compile + org.w3c.css:sac:jar:1.3:compile + org.yaml:snakeyaml:jar:1.14:compile + redis.clients:jedis:jar:2.5.2:compile diff --git a/3rd-dependencies/listDeps.sh b/3rd-dependencies/listDeps.sh new file mode 100755 index 000000000..3f6dddbd5 --- /dev/null +++ b/3rd-dependencies/listDeps.sh @@ -0,0 +1,8 @@ +# This script +cd .. +mvn dependency:list -DexcludeGroupIds=org.eclipse.hawkbit -Dsort=true -DoutputFile=dependencies.txt +find . -name dependencies.txt|while read i; do cat $i;done|grep '.*:.*:compile'|sort|uniq > 3rd-dependencies/compile.txt +find . -name dependencies.txt|while read i; do cat $i;done|grep '.*:.*:test'|sort|uniq > 3rd-dependencies/test.txt +find . -name dependencies.txt|while read i; do cat $i;done|grep '.*:.*:provided'|sort|uniq > 3rd-dependencies/provided.txt +find . -name dependencies.txt|while read i; do rm $i;done +cd 3rd-dependencies/ diff --git a/3rd-dependencies/provided.txt b/3rd-dependencies/provided.txt new file mode 100755 index 000000000..84074c22c --- /dev/null +++ b/3rd-dependencies/provided.txt @@ -0,0 +1,6 @@ + javax.servlet:javax.servlet-api:jar:3.1.0:provided + org.apache.tomcat.embed:tomcat-embed-core:jar:8.0.28:provided + org.apache.tomcat.embed:tomcat-embed-el:jar:8.0.28:provided + org.apache.tomcat.embed:tomcat-embed-logging-juli:jar:8.0.28:provided + org.apache.tomcat.embed:tomcat-embed-websocket:jar:8.0.28:provided + org.springframework.boot:spring-boot-starter-tomcat:jar:1.2.7.RELEASE:provided diff --git a/3rd-dependencies/test.txt b/3rd-dependencies/test.txt new file mode 100755 index 000000000..83d519ac8 --- /dev/null +++ b/3rd-dependencies/test.txt @@ -0,0 +1,55 @@ + com.fasterxml.jackson.core:jackson-core:jar:2.5.0:test + com.fasterxml.jackson.core:jackson-databind:jar:2.5.0:test + com.github.fge:btf:jar:1.2:test + com.github.fge:jackson-coreutils:jar:1.6:test + com.github.fge:json-patch:jar:1.7:test + com.github.fge:msg-simple:jar:1.1:test + com.google.code.findbugs:jsr305:jar:2.0.1:test + com.h2database:h2:jar:1.4.186:test + com.jayway.jsonpath:json-path:jar:0.9.1:test + commons-beanutils:commons-beanutils-core:jar:1.8.3:test + commons-io:commons-io:jar:2.4:test + commons-logging:commons-logging:jar:1.1.1:test + de.flapdoodle.embed:de.flapdoodle.embed.mongo:jar:1.50.0:test + de.flapdoodle.embed:de.flapdoodle.embed.process:jar:1.50.0:test + io.swagger:swagger-annotations:jar:1.5.0:test + javax.el:javax.el-api:jar:2.2.4:test + junit:junit:jar:4.12:test + net.java.dev.jna:jna-platform:jar:4.0.0:test + net.java.dev.jna:jna:jar:3.3.0:test + net.java.dev.jna:jna:jar:4.0.0:test + net.java.dev.jna:jna:jar:platform:3.3.0:test + net.minidev:json-smart:jar:1.2:test + org.apache.commons:commons-compress:jar:1.3:test + org.apache.commons:commons-lang3:jar:3.1:test + org.apache.tika:tika-core:jar:1.7:test + org.aspectj:aspectjrt:jar:1.8.5:test + org.atteo:evo-inflector:jar:1.2.1:test + org.easytesting:fest-assert-core:jar:2.0M10:test + org.easytesting:fest-assert:jar:1.4:test + org.easytesting:fest-util:jar:1.2.5:test + org.hamcrest:hamcrest-core:jar:1.3:test + org.hamcrest:hamcrest-library:jar:1.3:test + org.json:json:jar:20141113:test + org.jvnet.jaxb2_commons:jaxb2-basics-runtime:jar:0.9.3:test + org.mariadb.jdbc:mariadb-java-client:jar:1.2.3:test + org.mockito:mockito-core:jar:1.10.19:test + org.objenesis:objenesis:jar:2.1:test + org.springframework.boot:spring-boot-starter-test:jar:1.2.7.RELEASE:test + org.springframework.data:spring-data-rest-core:jar:2.3.1.RELEASE:test + org.springframework.data:spring-data-rest-webmvc:jar:2.3.1.RELEASE:test + org.springframework.hateoas:spring-hateoas:jar:0.16.0.RELEASE:test + org.springframework.plugin:spring-plugin-core:jar:1.1.0.RELEASE:test + org.springframework.security:spring-security-aspects:jar:3.2.8.RELEASE:test + org.springframework.security:spring-security-config:jar:3.2.8.RELEASE:test + org.springframework.security:spring-security-web:jar:3.2.8.RELEASE:test + org.springframework:spring-context-support:jar:4.1.8.RELEASE:test + org.springframework:spring-test:jar:4.1.8.RELEASE:test + org.springframework:spring-web:jar:4.1.8.RELEASE:test + org.springframework:spring-webmvc:jar:4.1.8.RELEASE:test + ru.yandex.qatools.allure:allure-java-adaptor-api:jar:1.4.15:test + ru.yandex.qatools.allure:allure-java-annotations:jar:1.4.15:test + ru.yandex.qatools.allure:allure-java-aspects:jar:1.4.15:test + ru.yandex.qatools.allure:allure-junit-adaptor:jar:1.4.15:test + ru.yandex.qatools.allure:allure-model:jar:1.4.15:test + ru.yandex.qatools.properties:properties-loader:jar:1.5:test diff --git a/CONTRUBUTING.md b/CONTRUBUTING.md new file mode 100755 index 000000000..68251d848 --- /dev/null +++ b/CONTRUBUTING.md @@ -0,0 +1,70 @@ +# Contributing to eclipse hawkBit + +:+1: First off, thanks for taking the time to contribute! We really appreciate this. :+1: + +Please read this if you intend to contribute to the project. + +## Code Conventions + +* Java files: + * we follow the standard eclipse IDE (built in) code formatter with the following changes: + * Tab policy: spaces only: 4 +* XML files: + * we follow the standard eclipse IDE XML formatter with the following changes: + * Indent using spaces only: 3 +* SCSS files: + * we follow the standard [scss-lint](https://github.com/brigade/scss-lint/) rules with the following exception: + * disabled rules: ImportantRule, PropertySortOrder +* Sonarqube: + * Our rule set is defined [here](http://sonar.eu-gb.mybluemix.net) + +## Legal considerations for your contribution + +The following steps are necessary to comply with the Eclipse Foundation's IP policy. + +Please also read [this](http://wiki.eclipse.org/Development_Resources/Contributing_via_Git) + +In order for any contributions to be accepted you MUST do the following things. + +* Sign the [Eclipse Foundation Contributor License Agreement](http://www.eclipse.org/legal/CLA.php). +To sign the Eclipse CLA you need to: + + * Obtain an Eclipse Foundation userid. Anyone who currently uses Eclipse Bugzilla or Gerrit systems already has one of those. +If you don’t, you need to [register](https://dev.eclipse.org/site_login/createaccount.php). + + * Login into the [projects portal](https://projects.eclipse.org/), select “My Account”, and then the “Contributor License Agreement” tab. + +* Add your github username in your Eclipse Foundation account settings. Log in it to Eclipse and go to account settings. + +* "Sign-off" your commits (NOTE! This is only necessary if contributing to the vertx-core project) + +Every commit you make in your patch or pull request MUST be "signed off". + +You do this by adding the `-s` flag when you make the commit(s), e.g. + + git commit -s -m "Shave the yak some more" + +## Making your changes + +* Fork the repository on GitHub +* Create a new branch for your changes +* Make your changes +* Make sure you include tests +* Make sure the tests pass after your changes +* Commit your changes into that branch +* Use descriptive and meaningful commit messages +* If you have a lot of commits squash them into a single commit +* Make sure you use the `-s` flag when committing as explained above. +* Push your changes to your branch in your forked repository + +## Submitting the changes + +Submit a pull request via the normal GitHub UI (desktop or web). + +## After submitting + +* Do not use your branch for any other development, otherwise further changes that you make will be visible in the PR. + +# Further information + +* [Eclipse Project Page](http://projects.eclipse.org/projects/iot.hawkbit) diff --git a/DDIA.md b/DDIA.md new file mode 100755 index 000000000..4754ae8c1 --- /dev/null +++ b/DDIA.md @@ -0,0 +1,131 @@ +## Direct Device Integration API (HTTP) +This API is based on HTTP standards and based on a polling mechanism. +The hawkbit update server provides REST resources which are consumed by the device to retrieve software update tasks. + + +##### Base Poll Resource +``` +GET /{tenant}/controller/v1/{controllerId} +``` +In the answer to the baseUrl polling the backend can send action links. A possible action is a deploy command. +The client makes a GET request on the given link: + +_Example Response_ +``` +{ + "config": { + "polling": { + "sleep": "00:05:00" + } + }, + "_links": { + "deploymentBase": { + "href": "http://localhost:8080/default/controller/v1/example/deploymentBase/1?c=644088541" + }, + "configData": { + "href": "http://localhost:8080/default/controller/v1/example/configData" + } + } +} +``` + +##### Deployment Base Resource +``` +GET /{tenant}/controller/v1/{controllerId}/deploymentBase/{id} +``` +_Example Response_ +``` +{ + "deployment": { + "download": "forced", + "update": "forced", + "chunks": [ + { + "part": "os", + "version": "1.0.0", + "name": "Linux", + "artifacts": [ + { + "filename": "linux.zip", + "hashes": { + "sha1": "46fc56de883ec027759d8513458fe1010aa7e793", + "md5": "5813e9655bd6871d0c25b8d510fd8605" + }, + "size": 52167, + "_links": { + "download": { + "href": "http://localhost:8080/default/controller/v1/example/softwaremodules/1/artifacts/linux.zip" + }, + "md5sum": { + "href": "http://localhost:8080/default/controller/v1/example/softwaremodules/1/artifacts/linux.zip.MD5SUM" + } + } + } + ] + } + ] + }, + "id": "1" +} +``` + + +##### Deployment Feedback Resource +To every deployment the client can post feedback back to the update-server about the deployment status. +``` +POST /{tenant}/controller/v1/{controllerId}/deploymentBase/{id}/feedback +Content-Type: application/json +``` +_Example Body Deployment Success_ +``` +{ + "id": 1, + "time": "20140511T121314", + "status": { + "execution": "closed", + "result": { + "finished": "success", + "progress": {} + } + } +} +``` + +_Example Body Deployment Proceeding_ +``` +{ + "id": "1", + "time": "20140511T121314", + "status": { + "execution": "proceeding", + "result": { + "finished": "none", + "progress": { + "cnt": 2, + "of": 5 + } + }, + "details": [ + "checking hash sums" + ] + } +} +``` + +_Example Body Deployment Error_ +``` +{ + "id": 1, + "time": "20140511T121314", + "status": { + "execution": "rejected", + "result": { + "finished": "failure", + "progress": {} + }, + "details": [ + "something bad happend" + ] + } +} +``` \ No newline at end of file diff --git a/LICENCE_HEADER_TEMPLATE.txt b/LICENCE_HEADER_TEMPLATE.txt new file mode 100755 index 000000000..111acb867 --- /dev/null +++ b/LICENCE_HEADER_TEMPLATE.txt @@ -0,0 +1,6 @@ +Copyright (c) 2015 Bosch Software Innovations GmbH and others. + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +which accompanies this distribution, and is available at +http://www.eclipse.org/legal/epl-v10.html diff --git a/README.md b/README.md new file mode 100755 index 000000000..2133089e1 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# hawkbit-update-server +Build Status: [![Circle CI](https://circleci.com/gh/bsinno/hawkbit-update-server/tree/master.svg?style=svg&circle-token=507140469c35791e36af04f5b8816ec79be4c82c)](https://circleci.com/gh/bsinno/hawkbit-update-server/tree/master) + +[hawkBit](https://projects.eclipse.org/projects/iot.hawkbit) is an domain independent back end solution for rolling out software updates to constrained edge devices as well as more powerful controllers and gateways connected to IP based networking infrastructure. + + +# Compile +``` +mvn install +``` + +## Try out examples +#### Standalone Test Application Server +[Example Application](examples/hawkbit-example-app) +#### Device Simulator using the DMF AMQP API +[Device Simulator](examples/hawkbit-device-simulator) + +# Modules +`hawkbit-core` : core elements. +`hawkbit-security-core` : core security elements. +`hawkbit-security-integration` : security integration elements to integrate security into hawkbit. +`hawkbit-artifact-repository-mongo` : artifact repository implementation to mongoDB. +`hawkbit-autoconfigure` : spring-boot auto-configuration. +`hawkbit-dmf-api` : API for the Device Management Integration. +`hawkbit-dmf-amqp` : AMQP endpoint implementation for the DMF API. +`hawkbit-repository` : repository implemenation based on SQL for all meta-data. +`hawkbit-http-security` : implementation for security filters for HTTP. +`hawkbit-rest-api` : API classes for the REST Management API. +`hawkbit-rest-resource` : HTTP REST endpoints for the Management and the Direct Device API. +`hawkbit-rest-resource` : Vaadin UI. +`hawkbit-cache-redis` : spring cache manager configuration and implementation with redis, distributed cache and distributed events. + + +# Device Integration +There are two device integration APIs provided by the hawkbit update server. +* [Direct Device Integration API (HTTP)](DDIA.md) +* Device Management Federation API (AMQP) diff --git a/circle.yml b/circle.yml new file mode 100755 index 000000000..bb5d6f292 --- /dev/null +++ b/circle.yml @@ -0,0 +1,33 @@ +# +# Copyright (c) 2015 Bosch Software Innovations GmbH and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# + +machine: + java: + version: oraclejdk8 + environment: + _JAVA_OPTIONS: "-Xms512m -Xmx1024m" +dependencies: + cache_directories: + - "~/.embedmongo" + override: + - mvn install -DskipTests +test: + override: + - sh sonarCircleCi.sh + post: + - mkdir -p $CIRCLE_TEST_REPORTS/junit/ + - find . -type f -regex ".*/target/surefire-reports/.*xml" -exec cp {} $CIRCLE_TEST_REPORTS/junit/ \; +general: + artifacts: + - "*.jar" + - "pom.xml" +notify: + webhooks: + # gitter hook + - url: https://webhooks.gitter.im/e/a20a6bc2bda5a8a77d39 diff --git a/eclipse_codeformatter.xml b/eclipse_codeformatter.xml new file mode 100755 index 000000000..b0852cb04 --- /dev/null +++ b/eclipse_codeformatter.xml @@ -0,0 +1,295 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/README.md b/examples/README.md new file mode 100755 index 000000000..320ee25cc --- /dev/null +++ b/examples/README.md @@ -0,0 +1,2 @@ +# Examples +TODO \ No newline at end of file diff --git a/examples/hawkbit-device-simulator/.gitignore b/examples/hawkbit-device-simulator/.gitignore new file mode 100755 index 000000000..09e3bc9b2 --- /dev/null +++ b/examples/hawkbit-device-simulator/.gitignore @@ -0,0 +1,2 @@ +/bin/ +/target/ diff --git a/examples/hawkbit-device-simulator/README.md b/examples/hawkbit-device-simulator/README.md new file mode 100755 index 000000000..caa86c3c0 --- /dev/null +++ b/examples/hawkbit-device-simulator/README.md @@ -0,0 +1,18 @@ +# hawkBit Device Simulator + +The device simulator handles software update commands from the update server. + +## Run +``` +java -jar examples/hawkbit-device-simulator/target/hawkbit-device-simulator-*-SNAPSHOT.jar +``` +Or: +``` +run org.eclipse.hawkbit.simulator.DeviceSimulator +``` + +## Usage +The device simulator exposes an REST-API which can be used to trigger device creation. +``` +http://localhost:8083/start?amount=10 +``` diff --git a/examples/hawkbit-device-simulator/pom.xml b/examples/hawkbit-device-simulator/pom.xml new file mode 100755 index 000000000..4bc38985e --- /dev/null +++ b/examples/hawkbit-device-simulator/pom.xml @@ -0,0 +1,61 @@ + + + + + 4.0.0 + + org.eclipse.hawkbit + 0.1.0-SNAPSHOT + hawkbit-examples-parent + + + hawkbit-device-simulator + hawkBit :: Device Simulator + Device Management Federation API based simulator + + + + org.eclipse.hawkbit + hawkbit-dmf-api + ${project.version} + + + org.springframework.amqp + spring-rabbit + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-log4j2 + + + org.springframework.boot + spring-boot-autoconfigure + + + + diff --git a/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/DeviceSimulator.java b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/DeviceSimulator.java new file mode 100755 index 000000000..aa74665dc --- /dev/null +++ b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/DeviceSimulator.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.simulator; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * The main-method to start the Spring-Boot application. + * + * + * + */ +@SpringBootApplication +public class DeviceSimulator { + + /** + * Start the Spring Boot Application. + * + * @param args + * the args + */ + public static void main(final String[] args) { + SpringApplication.run(DeviceSimulator.class, args); + } +} diff --git a/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/SimulationController.java b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/SimulationController.java new file mode 100755 index 000000000..ab8847cb1 --- /dev/null +++ b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/SimulationController.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.simulator; + +import org.eclipse.hawkbit.simulator.amqp.SpSenderService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * REST endpoint for controlling the device simulator. + * + * + * + */ +@RestController +public class SimulationController { + + @Autowired + private SpSenderService spSenderService; + + /** + * The start resource to start a device creation. + * + * @param name + * the name prefix of the generated device naming + * @param amount + * the amount of devices to be created + * @param tenant + * the tenant to create the device to + * @return a response string that devices has been created + */ + @RequestMapping("/start") + String start(@RequestParam(value = "name", defaultValue = "dmfSimulated") final String name, + @RequestParam(value = "amount", defaultValue = "20") final int amount, + @RequestParam(value = "tenant", defaultValue = "DEFAULT") final String tenant) { + + for (int i = 0; i < amount; i++) { + spSenderService.createOrUpdateThing(tenant, name + i); + } + + return "Updated " + amount + " DMF connected targets!"; + } +} diff --git a/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/AmqpConfiguration.java b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/AmqpConfiguration.java new file mode 100755 index 000000000..b99f24800 --- /dev/null +++ b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/AmqpConfiguration.java @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.simulator.amqp; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.FanoutExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * The spring AMQP configuration to use a AMQP for communication with SP update + * server. + * + * + * + */ +@Configuration +@EnableConfigurationProperties(AmqpProperties.class) +public class AmqpConfiguration { + + @Autowired + protected AmqpProperties amqpProperties; + + @Autowired + private ConnectionFactory connectionFactory; + + @Autowired + private RabbitTemplate rabbitTemplate; + + /** + * Create jackson message converter bean. + * + * @return the jackson message converter + */ + @Bean + public MessageConverter jsonMessageConverter() { + final Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter(); + rabbitTemplate.setMessageConverter(jackson2JsonMessageConverter); + return jackson2JsonMessageConverter; + } + + /** + * Create the receiver queue from sp. Receive messages from sp. + * + * @return the queue + */ + @Bean + public Queue receiverConnectorQueueFromSp() { + return new Queue(amqpProperties.getReceiverConnectorQueueFromSp(), true, false, false, + getDeadLetterExchangeArgs()); + } + + /** + * Create the recevier exchange. Sp send messages to this exchange. + * + * @return the exchange + */ + @Bean + public FanoutExchange exchangeQueueToConnector() { + return new FanoutExchange(amqpProperties.getSenderForSpExchange()); + } + + /** + * Create the Binding + * {@link AmqpConfiguration#receiverConnectorQueueFromSp()} to + * {@link AmqpConfiguration#exchangeQueueToConnector()}. + * + * @return the binding and create the queue and exchange + */ + @Bean + public Binding bindReceiverQueueToSpExchange() { + return BindingBuilder.bind(receiverConnectorQueueFromSp()).to(exchangeQueueToConnector()); + } + + /** + * Create dead letter queue. + * + * @return the queue + */ + @Bean + public Queue deadLetterQueue() { + return new Queue(amqpProperties.getDeadLetterQueue(), true, false, true); + } + + /** + * Create the dead letter fanout exchange. + * + * @return the fanout exchange + */ + @Bean + public FanoutExchange exchangeDeadLetter() { + return new FanoutExchange(amqpProperties.getDeadLetterExchange()); + } + + /** + * Create the Binding deadLetterQueue to exchangeDeadLetter. + * + * @return the binding + */ + @Bean + public Binding bindDeadLetterQueueToLwm2mExchange() { + return BindingBuilder.bind(deadLetterQueue()).to(exchangeDeadLetter()); + } + + /** + * Returns the Listener factory. + * + * @return the {@link SimpleMessageListenerContainer} that gets used receive + * AMQP messages + */ + @Bean(name = { "listenerContainerFactory" }) + public SimpleRabbitListenerContainerFactory listenerContainerFactory() { + final SimpleRabbitListenerContainerFactory containerFactory = new SimpleRabbitListenerContainerFactory(); + containerFactory.setDefaultRequeueRejected(false); + containerFactory.setConnectionFactory(connectionFactory); + return containerFactory; + } + + private Map getDeadLetterExchangeArgs() { + final Map args = new HashMap(); + args.put("x-dead-letter-exchange", amqpProperties.getDeadLetterExchange()); + return args; + } + +} diff --git a/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/AmqpProperties.java b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/AmqpProperties.java new file mode 100755 index 000000000..ff9762c5d --- /dev/null +++ b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/AmqpProperties.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.simulator.amqp; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Bean which holds the necessary properties for configuring the AMQP + * connection. + * + * + * + */ +@ConfigurationProperties("hawkbit.device.simulator.amqp") +public class AmqpProperties { + + private String receiverConnectorQueueFromSp; + private String senderForSpExchange; + + private String deadLetterQueue; + private String deadLetterExchange; + + public String getReceiverConnectorQueueFromSp() { + return receiverConnectorQueueFromSp; + } + + public void setReceiverConnectorQueueFromSp(final String receiverConnectorQueueFromSp) { + this.receiverConnectorQueueFromSp = receiverConnectorQueueFromSp; + } + + public String getDeadLetterExchange() { + return deadLetterExchange; + } + + public void setDeadLetterExchange(final String deadLetterExchange) { + this.deadLetterExchange = deadLetterExchange; + } + + public String getDeadLetterQueue() { + return deadLetterQueue; + } + + public void setDeadLetterQueue(final String deadLetterQueue) { + this.deadLetterQueue = deadLetterQueue; + } + + public String getSenderForSpExchange() { + return senderForSpExchange; + } + + public void setSenderForSpExchange(final String senderForSpExchange) { + this.senderForSpExchange = senderForSpExchange; + } +} diff --git a/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/MessageHandlerCacheConstant.java b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/MessageHandlerCacheConstant.java new file mode 100755 index 000000000..fd287b3a3 --- /dev/null +++ b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/MessageHandlerCacheConstant.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.simulator.amqp; + +/** + * The constants for the caching. + * + * + * + */ +public final class MessageHandlerCacheConstant { + + /** + * The name of the cache. + */ + public static final String CACHE_NAME = "hawkbit.device.simulator.states"; + + private MessageHandlerCacheConstant() { + + } +} diff --git a/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/MessageService.java b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/MessageService.java new file mode 100755 index 000000000..bd48bbe12 --- /dev/null +++ b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/MessageService.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.simulator.amqp; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.AbstractJavaTypeMapper; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Abstract class for sender and receiver service. + * + * + * + */ +public class MessageService { + + private static final Logger LOGGER = LoggerFactory.getLogger(MessageService.class); + + protected RabbitTemplate rabbitTemplate; + + protected AmqpProperties amqpProperties; + + /** + * Constructor. + * + * @param rabbitTemplate + * the rabbit template + * @param amqpProperties + * the amqp properties + * @param messageConverter + * the message converter + */ + @Autowired + public MessageService(final RabbitTemplate rabbitTemplate, final AmqpProperties amqpProperties) { + this.rabbitTemplate = rabbitTemplate; + this.amqpProperties = amqpProperties; + } + + public void setAmqpProperties(final AmqpProperties amqpProperties) { + this.amqpProperties = amqpProperties; + } + + public void setRabbitTemplate(final RabbitTemplate rabbitTemplate) { + this.rabbitTemplate = rabbitTemplate; + } + + /** + * Method to call when error emerges. + * + * @param message + * the message that triggered the error + * @param error + * the error + */ + public void logAndThrowMessageError(final Message message, final String error) { + LOGGER.error("Error \"{}\" reported by message {}", error, message.getMessageProperties().getMessageId()); + throw new IllegalArgumentException(error); + } + + /** + * Convert a message body to a given class and set the message header + * AbstractJavaTypeMapper.DEFAULT_CLASSID_FIELD_NAME for Jackson converter. + * + * @param message + * which body will converted + * @param clazz + * the body class + * @return the converted body + */ + @SuppressWarnings("unchecked") + public T convertMessage(final Message message, final Class clazz) { + message.getMessageProperties().getHeaders().put(AbstractJavaTypeMapper.DEFAULT_CLASSID_FIELD_NAME, + clazz.getTypeName()); + return (T) rabbitTemplate.getMessageConverter().fromMessage(message); + } + + /** + * Method to verify if lwm2m header is set. + * + * @param message + * the message with the header + * @param header + * the header to verify + */ + public void checkIfLwm2mHeaderEmpty(final Message message, final String header) { + final Object headerObject = message.getMessageProperties().getHeaders().get(header); + if (null == headerObject) { + logAndThrowMessageError(message, "Header of " + header + "empty."); + } + + } + +} diff --git a/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/ReceiverService.java b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/ReceiverService.java new file mode 100755 index 000000000..f5c02789e --- /dev/null +++ b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/ReceiverService.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.simulator.amqp; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Abstract class for all receiver objects. + * + * + * + */ +public abstract class ReceiverService extends MessageService { + + /** + * Constructor. + * + * @param rabbitTemplate + * RabbitTemplate + * @param amqpProperties + * AmqpProperties + * @param messageConverter + * MessageConverter + */ + @Autowired + public ReceiverService(final RabbitTemplate rabbitTemplate, final AmqpProperties amqpProperties) { + super(rabbitTemplate, amqpProperties); + } + + /** + * Method to validate if content type is set in the message properties. + * + * @param message + * the message to get validated + */ + protected void checkContentTypeJson(final Message message) { + if (message.getBody().length == 0) { + return; + } + final MessageProperties messageProperties = message.getMessageProperties(); + final String headerContentType = (String) messageProperties.getHeaders().get("content-type"); + if (null != headerContentType) { + messageProperties.setContentType(headerContentType); + } + final String contentType = messageProperties.getContentType(); + if (contentType != null && contentType.contains("json")) { + return; + } + throw new IllegalArgumentException("Content-Type is not JSON compatible"); + } + +} diff --git a/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/SenderService.java b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/SenderService.java new file mode 100755 index 000000000..e8b82b6f0 --- /dev/null +++ b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/SenderService.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.simulator.amqp; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Abstract calls for sender service objects. + * + * + * + */ +public abstract class SenderService extends MessageService { + + /** + * Constructor for sender service. + * + * @param rabbitTemplate + * the rabbit template + * @param amqpProperties + * the amqp properties + * @param cacheManager + * the cache manager + */ + @Autowired + public SenderService(final RabbitTemplate rabbitTemplate, final AmqpProperties amqpProperties) { + super(rabbitTemplate, amqpProperties); + } + + /** + * Send a message if the message is not null. + * + * @param adress + * the exchange name + * @param message + * the amqp message which will be send if its not null + */ + public void sendMessage(final String adress, final Message message) { + if (message == null) { + return; + } + rabbitTemplate.setExchange(adress); + rabbitTemplate.send(message); + } + + /** + * Convert object and message properties to message. + * + * @param object + * to get converted + * @param messageProperties + * to get converted + * @return converted message + */ + public Message convertMessage(final Object object, final MessageProperties messageProperties) { + return rabbitTemplate.getMessageConverter().toMessage(object, messageProperties); + } + +} diff --git a/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/SimulatedUpdate.java b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/SimulatedUpdate.java new file mode 100755 index 000000000..3deefcf46 --- /dev/null +++ b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/SimulatedUpdate.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.simulator.amqp; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * Object for holding attributes for a simulated update for the device + * simulator. + * + * + * + */ +public class SimulatedUpdate implements Serializable { + + private static final long serialVersionUID = 1L; + + protected final String tenant; + protected final String thingId; + + protected final Long actionId; + + protected LocalDateTime startCacheTime; + + /** + * Constructor of the class. + * + * @param tenant + * the tenant name. + * @param thingId + * the thing id. + * @param actionId + * the action id + * @param softwareModuleId + * software modul id + * @param urls + * the artifact download urls. + */ + public SimulatedUpdate(final String tenant, final String thingId, final Long actionId) { + this.tenant = tenant; + this.thingId = thingId; + this.actionId = actionId; + this.startCacheTime = LocalDateTime.now(); + } + + public String getTenant() { + return tenant; + } + + public String getThingId() { + return thingId; + } + + public Long getActionId() { + return actionId; + } + + public LocalDateTime getStartCacheTime() { + return startCacheTime; + } +} diff --git a/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/SpReceiverService.java b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/SpReceiverService.java new file mode 100755 index 000000000..3cf8891f0 --- /dev/null +++ b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/SpReceiverService.java @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.simulator.amqp; + +import java.util.Map; + +import org.eclipse.hawkbit.dmf.amqp.api.EventTopic; +import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey; +import org.eclipse.hawkbit.dmf.amqp.api.MessageType; +import org.eclipse.hawkbit.dmf.json.model.ActionStatus; +import org.eclipse.hawkbit.dmf.json.model.DownloadAndUpdateRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.stereotype.Component; + +/** + * Handle all incoming Messages from SP. + * + * + * + */ +@Component +public class SpReceiverService extends ReceiverService { + + private static final Logger LOGGER = LoggerFactory.getLogger(ReceiverService.class); + + public static final String SOFTWARE_MODULE_FIRMWARE = "firmware"; + + private final SpSenderService spSenderService; + + /** + * Constructor. + * + * @param rabbitTemplate + * the rabbit template + * @param amqpProperties + * the amqp properties + * @param lwm2mSenderService + * the lwm2mSenderService + * @param spSenderService + * the spSenderService + */ + @Autowired + public SpReceiverService(final RabbitTemplate rabbitTemplate, final AmqpProperties amqpProperties, + final SpSenderService spSenderService) { + super(rabbitTemplate, amqpProperties); + this.spSenderService = spSenderService; + + } + + /** + * Handle the incoming Message from Queue with the property + * (com.bosch.sp.lwm2m.connector.amqp.receiverConnectorQueueFromSp). + * + * @param message + * the incoming message + * @param type + * the action type + * @param contentType + * the content type in message header + * @param thingId + * the thing id in message header + */ + @RabbitListener(queues = "${hawkbit.device.simulator.amqp.receiverConnectorQueueFromSp}", containerFactory = "listenerContainerFactory") + public void recieveMessageSp(final Message message, @Header(MessageHeaderKey.TYPE) final String type, + @Header(MessageHeaderKey.THING_ID) final String thingId) { + checkContentTypeJson(message); + delegateMessage(message, type, thingId); + } + + private void delegateMessage(final Message message, final String type, final String thingId) { + final MessageType messageType = MessageType.valueOf(type); + + switch (messageType) { + case EVENT: + handleEventMessage(message, thingId); + break; + default: + LOGGER.info("No valid message type property."); + break; + } + } + + private void handleEventMessage(final Message message, final String thingId) { + final Object eventHeader = message.getMessageProperties().getHeaders().get(MessageHeaderKey.TOPIC); + if (eventHeader == null) { + logAndThrowMessageError(message, "Event Topic is not set"); + } + final EventTopic eventTopic = EventTopic.valueOf(eventHeader.toString()); + switch (eventTopic) { + case DOWNLOAD_AND_INSTALL: + handleUpdateProcess(message, thingId); + break; + case CANCEL_DOWNLOAD: + handleCancelDownloadAction(message, thingId); + break; + default: + LOGGER.info("No valid event property."); + break; + } + } + + private void handleCancelDownloadAction(final Message message, final String thingId) { + final MessageProperties messageProperties = message.getMessageProperties(); + final Map headers = messageProperties.getHeaders(); + final String tenant = (String) headers.get(MessageHeaderKey.TENANT); + final Long actionId = convertMessage(message, Long.class); + + final SimulatedUpdate update = new SimulatedUpdate(tenant, thingId, actionId); + spSenderService.finishUpdateProcess(update, "Simulation canceled"); + } + + private void handleUpdateProcess(final Message message, final String thingId) { + final MessageProperties messageProperties = message.getMessageProperties(); + final Map headers = messageProperties.getHeaders(); + final String tenant = (String) headers.get(MessageHeaderKey.TENANT); + + final DownloadAndUpdateRequest downloadAndUpdateRequest = convertMessage(message, + DownloadAndUpdateRequest.class); + final Long actionId = downloadAndUpdateRequest.getActionId(); + + spSenderService.sendActionStatusMessage(tenant, ActionStatus.RUNNING, + "device Simulator retrieved update request. proceeding with simulation.", actionId); + + final SimulatedUpdate update = new SimulatedUpdate(tenant, thingId, actionId); + + spSenderService.finishUpdateProcess(update, "Simulation complete!"); + } + +} diff --git a/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/SpSenderService.java b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/SpSenderService.java new file mode 100755 index 000000000..1a66c2aa1 --- /dev/null +++ b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/amqp/SpSenderService.java @@ -0,0 +1,224 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.simulator.amqp; + +import java.util.Map; + +import org.eclipse.hawkbit.dmf.amqp.api.AmqpSettings; +import org.eclipse.hawkbit.dmf.amqp.api.EventTopic; +import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey; +import org.eclipse.hawkbit.dmf.amqp.api.MessageType; +import org.eclipse.hawkbit.dmf.json.model.ActionStatus; +import org.eclipse.hawkbit.dmf.json.model.ActionUpdateStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +/** + * Sender service to send message to SP. + * + * + * + */ +@Service +public class SpSenderService extends SenderService { + + private static final Logger LOGGER = LoggerFactory.getLogger(SpSenderService.class); + + private final String spExchange; + + /** + * + * @param rabbitTemplate + * the rabbit template + * @param amqpProperties + * the amqp properties + */ + @Autowired + public SpSenderService(final RabbitTemplate rabbitTemplate, final AmqpProperties amqpProperties) { + super(rabbitTemplate, amqpProperties); + this.spExchange = AmqpSettings.DMF_EXCHANGE; + } + + /** + * Finish the update process. This will send a action status to SP. + * + * @param update + * the simulated update object + * @param description + * a description according the update process + */ + public void finishUpdateProcess(final SimulatedUpdate update, final String description) { + final Message updateResultMessage = createUpdateResultMessage(update, ActionStatus.FINISHED, description); + sendMessage(spExchange, updateResultMessage); + } + + /** + * Finish update process with error and send error to SP. + * + * @param update + * the simulated update object + * @param messageDescription + * a description according the update process + */ + public void finishUpdateProcessWithError(final SimulatedUpdate update, final String messageDescription) { + sendErrorgMessage(update, messageDescription); + LOGGER.debug("Update process finished with error \"{}\" reported by thing {}", messageDescription, + update.getThingId()); + } + + /** + * Send an error message to SP. + * + * @param tenant + * the tenant + * @param messageDescription + * the error message description to send + * @param actionId + * the ID of the action for the error message + */ + public void sendErrorMessage(final String tenant, final String messageDescription, final Long actionId) { + final Message message = createActionStatusMessage(tenant, ActionStatus.ERROR, messageDescription, actionId); + sendMessage(spExchange, message); + } + + /** + * Send a warning message to SP. + * + * @param update + * the simulated update object + * @param warningMessage + * a warning description + */ + public void sendWarningMessage(final SimulatedUpdate update, final String warningMessage) { + final Message message = createActionStatusMessage(update, warningMessage, ActionStatus.WARNING); + sendMessage(spExchange, message); + } + + /** + * Method to send a action status to SP. + * + * @param tenant + * the tenant + * @param actionStatus + * the action status + * @param actionMessage + * the message to get send + * @param actionId + * the cached value + */ + public void sendActionStatusMessage(final String tenant, final ActionStatus actionStatus, + final String actionMessage, final Long actionId) { + final Message message = createActionStatusMessage(tenant, actionStatus, actionMessage, actionId); + sendMessage(message); + + } + + /** + * Create new thing created message and send to SP. + * + * @param tenant + * the tenant to create the target + * @param targetId + * the ID of the target to create or update + */ + public void createOrUpdateThing(final String tenant, final String targetId) { + + final MessageProperties messagePropertiesForSP = new MessageProperties(); + messagePropertiesForSP.setHeader(MessageHeaderKey.TYPE, MessageType.THING_CREATED.name()); + messagePropertiesForSP.setHeader(MessageHeaderKey.TENANT, tenant); + messagePropertiesForSP.setHeader(MessageHeaderKey.THING_ID, targetId); + messagePropertiesForSP.setHeader(MessageHeaderKey.SENDER, "simulator"); + messagePropertiesForSP.setContentType(MessageProperties.CONTENT_TYPE_JSON); + messagePropertiesForSP.setReplyTo(amqpProperties.getSenderForSpExchange()); + final Message convertedMessage = new Message(null, messagePropertiesForSP); + sendMessage(spExchange, convertedMessage); + LOGGER.debug("Created thing created message and send to SP for Thing \"{}\"", targetId); + } + + /** + * Send a created message to SP. + * + * @param message + * the message to get send + */ + private void sendMessage(final Message message) { + sendMessage(spExchange, message); + } + + /** + * Send error message to SP. + * + * @param context + * the current context + * @param messageDescription + * a description according the update process + */ + private void sendErrorgMessage(final SimulatedUpdate update, final String messageDescription) { + final Message message = createActionStatusMessage(update, messageDescription, ActionStatus.ERROR); + sendMessage(spExchange, message); + } + + /** + * Create a action status message. + * + * @param actionStatus + * the ActionStatus + * @param actionMessage + * the message description + * @param actionId + * the action id + * @param cacheValue + * the cacheValue value + */ + private Message createActionStatusMessage(final String tenant, final ActionStatus actionStatus, + final String actionMessage, final Long actionId) { + final MessageProperties messageProperties = new MessageProperties(); + final Map headers = messageProperties.getHeaders(); + final ActionUpdateStatus actionUpdateStatus = new ActionUpdateStatus(); + actionUpdateStatus.setActionStatus(actionStatus); + headers.put(MessageHeaderKey.TYPE, MessageType.EVENT.name()); + headers.put(MessageHeaderKey.TENANT, tenant); + headers.put(MessageHeaderKey.TOPIC, EventTopic.UPDATE_ACTION_STATUS.name()); + headers.put(MessageHeaderKey.CONTENT_TYPE, MessageProperties.CONTENT_TYPE_JSON); + if (!StringUtils.isEmpty(actionMessage)) { + actionUpdateStatus.getMessage().add(actionMessage); + } + actionUpdateStatus.setActionId(actionId); + return convertMessage(actionUpdateStatus, messageProperties); + } + + private Message createUpdateResultMessage(final SimulatedUpdate cacheValue, final ActionStatus actionStatus, + final String updateResultMessage) { + final MessageProperties messageProperties = new MessageProperties(); + final Map headers = messageProperties.getHeaders(); + final ActionUpdateStatus actionUpdateStatus = new ActionUpdateStatus(); + actionUpdateStatus.setActionStatus(actionStatus); + headers.put(MessageHeaderKey.TYPE, MessageType.EVENT.name()); + headers.put(MessageHeaderKey.TENANT, cacheValue.getTenant()); + headers.put(MessageHeaderKey.TOPIC, EventTopic.UPDATE_ACTION_STATUS.name()); + headers.put(MessageHeaderKey.CONTENT_TYPE, MessageProperties.CONTENT_TYPE_JSON); + actionUpdateStatus.getMessage().add(updateResultMessage); + actionUpdateStatus.setActionId(cacheValue.getActionId()); + return convertMessage(actionUpdateStatus, messageProperties); + } + + private Message createActionStatusMessage(final SimulatedUpdate update, final String messageDescription, + final ActionStatus status) { + final Message sendMessage = createActionStatusMessage(update.getTenant(), status, messageDescription, + update.getActionId()); + return sendMessage; + } + +} diff --git a/examples/hawkbit-device-simulator/src/main/resources/application.properties b/examples/hawkbit-device-simulator/src/main/resources/application.properties new file mode 100755 index 000000000..eaac2999e --- /dev/null +++ b/examples/hawkbit-device-simulator/src/main/resources/application.properties @@ -0,0 +1,101 @@ +# +# Copyright (c) 2015 Bosch Software Innovations GmbH and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# + + +######################################################################################### +# PUBLIC configuration, i.e. can be changed by users at runtime (defaults provided here) +######################################################################################### +## Configuration for RabbitMQ communication +hawkbit.device.simulator.amqp.receiverConnectorQueueFromSp=sp_connector_receiver +hawkbit.device.simulator.amqp.deadLetterQueue=simulator_deadletter +hawkbit.device.simulator.amqp.deadLetterExchange=simulator.deadletter +hawkbit.device.simulator.amqp.senderForSpExchange=simulator.replyTo + + +## Configuration for RabbitMQ integration +spring.rabbitmq.username=guest +spring.rabbitmq.password=guest +spring.rabbitmq.virtualHost=/ +spring.rabbitmq.host=localhost +spring.rabbitmq.port=5672 +spring.rabbitmq.dynamic=true + + +#disable expose jmx beans +spring.jmx.enabled=false +# the context path of the monitor spring actuator which offers following endpoints +# /autoconfig, /beans, /configprops, /dump, /env, /health, /info, /metrics, /mappings, /trace +management.context-path=/system +management.security.enabled=true +management.security.sessions=stateless +# ENDPOINTS +endpoints.autoconfig.id=autoconfig +endpoints.autoconfig.sensitive=true +endpoints.autoconfig.enabled=false +endpoints.beans.id=beans +endpoints.beans.sensitive=true +endpoints.beans.enabled=false +endpoints.configprops.id=configprops +endpoints.configprops.sensitive=true +endpoints.configprops.enabled=true +#endpoints.configprops.keys-to-sanitize=password,secret,key # suffix or regex +endpoints.dump.id=dump +endpoints.dump.sensitive=true +endpoints.dump.enabled=false +endpoints.env.id=env +endpoints.env.sensitive=true +endpoints.env.enabled=true +#endpoints.env.keys-to-sanitize=password,secret,key # suffix or regex +endpoints.health.id=health +endpoints.health.sensitive=true +endpoints.health.enabled=true +endpoints.info.id=info +endpoints.info.sensitive=true +endpoints.info.enabled=true +endpoints.metrics.id=metrics +endpoints.metrics.sensitive=true +endpoints.metrics.enabled=true +endpoints.shutdown.id=shutdown +endpoints.shutdown.sensitive=true +endpoints.shutdown.enabled=false +endpoints.trace.id=trace +endpoints.trace.sensitive=true +endpoints.trace.enabled=true + +# HEALTH INDICATORS (previously health.*) +management.health.db.enabled=true +management.health.diskspace.enabled=true +management.health.mongo.enabled=true +management.health.rabbit.enabled=true +management.health.redis.enabled=false +management.health.solr.enabled=false +management.health.diskspace.path=. +management.health.diskspace.threshold=10485760 +management.health.status.order=DOWN, OUT_OF_SERVICE, UNKNOWN, UP + +# SECURITY (SecurityProperties) +security.user.name=${BASIC_USERNAME:admin} +security.user.password=${BASIC_PASSWORD:admin} +security.user.role=USER +security.require-ssl=false +security.enable-csrf=false +security.basic.enabled=true +security.basic.realm=DeviceSimulator +security.basic.path= /system/** +security.basic.authorize-mode=ROLE +security.filter-order=0 +security.headers.xss=false +security.headers.cache=false +security.headers.frame=false +security.headers.content-type=false +security.headers.hsts=all +security.sessions=stateless +security.ignored= + +server.port=8083 diff --git a/examples/hawkbit-example-app/.gitignore b/examples/hawkbit-example-app/.gitignore new file mode 100755 index 000000000..b83d22266 --- /dev/null +++ b/examples/hawkbit-example-app/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/examples/hawkbit-example-app/README.md b/examples/hawkbit-example-app/README.md new file mode 100755 index 000000000..eaf30f306 --- /dev/null +++ b/examples/hawkbit-example-app/README.md @@ -0,0 +1,15 @@ +# hawkBit Example Application +The hawkBit example application is a standalone spring-boot application with an embedded servlet container to start the hawkBit. + +## Run +``` +java -jar examples/hawkbit-example-app/target/hawkbit-example-app-*-SNAPSHOT.jar +``` +Or: +``` +run org eclipse.hawkbit.app.Start +``` + +## Usage +The UI can be accessed via _http://localhost:8080/UI_. +The REST API can be accessed via _http://localhost:8080/rest/v1_. diff --git a/examples/hawkbit-example-app/pom.xml b/examples/hawkbit-example-app/pom.xml new file mode 100755 index 000000000..ed4403cc5 --- /dev/null +++ b/examples/hawkbit-example-app/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + org.eclipse.hawkbit + hawkbit-examples-parent + 0.1.0-SNAPSHOT + + hawkbit-example-app + hawkBit :: Example App + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + ${baseDir} + false + org.eclipse.hawkbit.app.Start + JAR + + + + + + + + + + + org.eclipse.hawkbit + hawkbit-autoconfigure + ${project.version} + + + org.eclipse.hawkbit + hawkbit-dmf-amqp + ${project.version} + + + org.eclipse.hawkbit + hawkbit-ui + ${project.version} + + + org.eclipse.hawkbit + hawkbit-security-integration + ${project.version} + + + org.eclipse.hawkbit + hawkbit-http-security + ${project.version} + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-log4j2 + + + org.springframework.security + spring-security-aspects + + + io.springfox + springfox-swagger2 + + + com.h2database + h2 + + + + diff --git a/examples/hawkbit-example-app/src/main/java/org/eclipse/hawkbit/app/MyLoginUI.java b/examples/hawkbit-example-app/src/main/java/org/eclipse/hawkbit/app/MyLoginUI.java new file mode 100755 index 000000000..763e8447e --- /dev/null +++ b/examples/hawkbit-example-app/src/main/java/org/eclipse/hawkbit/app/MyLoginUI.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.app; + +import org.eclipse.hawkbit.ui.login.HawkbitLoginUI; +import org.eclipse.hawkbit.ui.themes.HawkbitTheme; + +import com.vaadin.spring.annotation.SpringUI; + +/** + * Example hawkBit login UI implementation. + * + * A {@link SpringUI} annotated class must be present in the classpath for the + * login path. The easiest way to get an hawkBit login UI running is to extend + * the {@link HawkbitLoginUI} and to annotated it with {@link SpringUI} as in + * this example to the defined {@link HawkbitTheme#LOGIN_UI_PATH}. + * + * + * + */ +@SpringUI(path = HawkbitTheme.LOGIN_UI_PATH) +public class MyLoginUI extends HawkbitLoginUI { + + private static final long serialVersionUID = 1L; + +} diff --git a/examples/hawkbit-example-app/src/main/java/org/eclipse/hawkbit/app/MyUI.java b/examples/hawkbit-example-app/src/main/java/org/eclipse/hawkbit/app/MyUI.java new file mode 100755 index 000000000..cafe0749d --- /dev/null +++ b/examples/hawkbit-example-app/src/main/java/org/eclipse/hawkbit/app/MyUI.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.app; + +import org.eclipse.hawkbit.eventbus.EventSubscriber; +import org.eclipse.hawkbit.eventbus.event.EntityEvent; +import org.eclipse.hawkbit.ui.DispatcherRunnable; +import org.eclipse.hawkbit.ui.HawkbitUI; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.vaadin.spring.events.EventBus.SessionEventBus; + +import com.google.common.eventbus.AllowConcurrentEvents; +import com.google.common.eventbus.Subscribe; +import com.vaadin.annotations.Push; +import com.vaadin.server.VaadinSession; +import com.vaadin.server.VaadinSession.State; +import com.vaadin.server.WrappedSession; +import com.vaadin.shared.communication.PushMode; +import com.vaadin.shared.ui.ui.Transport; +import com.vaadin.spring.annotation.SpringUI; + +/** + * Example hawkBit UI implementation. + * + * A {@link SpringUI} annotated class must be present in the classpath. The + * easiest way to get an hawkBit UI running is to extend the {@link HawkbitUI} + * and to annotated it with {@link SpringUI} as in this example. + * + * + * + */ +@SpringUI +@Push(value = PushMode.AUTOMATIC, transport = Transport.WEBSOCKET) +@EventSubscriber +public class MyUI extends HawkbitUI { + + private static final long serialVersionUID = 1L; + + /** + * An {@link com.google.common.eventbus.EventBus} subscriber which + * subscribes {@link EntityEvent} from the repository to dispatch these + * events to the UI {@link SessionEventBus}. + * + * @param event + * the entity event which has been published from the repository + */ + @Override + @Subscribe + @AllowConcurrentEvents + public void dispatch(final org.eclipse.hawkbit.eventbus.event.Event event) { + final VaadinSession session = getSession(); + if (session != null && session.getState() == State.OPEN) { + final WrappedSession wrappedSession = session.getSession(); + if (wrappedSession != null) { + final SecurityContext userContext = (SecurityContext) wrappedSession + .getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); + if (eventSecurityCheck(userContext, event)) { + final SecurityContext oldContext = SecurityContextHolder.getContext(); + try { + access(new DispatcherRunnable(eventBus, session, userContext, event)); + } finally { + SecurityContextHolder.setContext(oldContext); + } + } + } + } + } + +} diff --git a/examples/hawkbit-example-app/src/main/java/org/eclipse/hawkbit/app/Start.java b/examples/hawkbit-example-app/src/main/java/org/eclipse/hawkbit/app/Start.java new file mode 100755 index 000000000..b87b53fef --- /dev/null +++ b/examples/hawkbit-example-app/src/main/java/org/eclipse/hawkbit/app/Start.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.app; + +import org.eclipse.hawkbit.RepositoryApplicationConfiguration; +import org.eclipse.hawkbit.autoconfigure.security.EnableHawkbitManagedSecurityConfiguration; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; + +/** + * A {@link SpringBootApplication} annoated class with a main method to start. + * The minimal configuration for the standalone hawkBit server. + * + * + * + */ +@SpringBootApplication +@Import({ RepositoryApplicationConfiguration.class }) +@EnableHawkbitManagedSecurityConfiguration +public class Start { + + /** + * Main method to start the spring-boot application. + * + * @param args + * the VM arguments. + */ + public static void main(final String[] args) { + SpringApplication.run(Start.class, args); + } +} diff --git a/examples/hawkbit-example-app/src/main/resources/application.properties b/examples/hawkbit-example-app/src/main/resources/application.properties new file mode 100755 index 000000000..7149dbb9c --- /dev/null +++ b/examples/hawkbit-example-app/src/main/resources/application.properties @@ -0,0 +1,29 @@ +# +# Copyright (c) 2015 Bosch Software Innovations GmbH and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# + +# need to re-name these properties in the defaulthawkbit.properties and code! +hawkbit.server.controller.security.authentication.anonymous.enabled=true +hawkbit.server.controller.security.authentication.header.enabled=false +hawkbit.server.controller.security.authentication.targettoken.enabled=false +hawkbit.server.controller.security.authentication.gatewaytoken.enabled=false + +spring.profiles.active=amqp + +vaadin.servlet.params.productionMode=false +vaadin.static.servlet.params.productionMode=false + +## Configuration for RabbitMQ integration +hawkbit.server.amqp.username=guest +hawkbit.server.amqp.password=guest +hawkbit.server.amqp.virtualHost=/ +hawkbit.server.amqp.host=localhost +hawkbit.server.amqp.port=5672 +hawkbit.server.amqp.deadLetterQueue=sp_deadletter +hawkbit.server.amqp.deadLetterExchange=sp.deadletter +hawkbit.server.amqp.receiverQueue=sp_receiver diff --git a/examples/pom.xml b/examples/pom.xml new file mode 100755 index 000000000..b4a99b16f --- /dev/null +++ b/examples/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + + org.eclipse.hawkbit + hawkbit-parent + 0.1.0-SNAPSHOT + + + hawkbit-examples-parent + hawkBit :: Examples Parent + pom + + + hawkbit-device-simulator + hawkbit-example-app + + + \ No newline at end of file diff --git a/hawkbit-artifact-repository-mongo/README.md b/hawkbit-artifact-repository-mongo/README.md new file mode 100755 index 000000000..3f05d04e6 --- /dev/null +++ b/hawkbit-artifact-repository-mongo/README.md @@ -0,0 +1,3 @@ +# HawkBit Artifact Repositoy +HawkBit Artifact Repository is library for storing binary artifacts and metadata into MongoDB. +It has an spring-boot autoconfiguration for easily integration into spring-boot projects. \ No newline at end of file diff --git a/hawkbit-artifact-repository-mongo/pom.xml b/hawkbit-artifact-repository-mongo/pom.xml new file mode 100755 index 000000000..8eb7534b2 --- /dev/null +++ b/hawkbit-artifact-repository-mongo/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + org.eclipse.hawkbit + 0.1.0-SNAPSHOT + hawkbit-parent + + hawkbit-artifact-repository-mongo + hawkBit :: Artifact Repository Mongo + + + + + org.eclipse.hawkbit + hawkbit-core + ${project.version} + + + org.springframework.data + spring-data-mongodb + + + org.springframework + spring-context + + + org.springframework.boot + spring-boot-autoconfigure + + + com.google.guava + guava + 18.0 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework + spring-test + test + + + org.easytesting + fest-assert-core + test + + + ru.yandex.qatools.allure + allure-junit-adaptor + test + + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + test + + + + + org.mockito + mockito-core + test + + + + \ No newline at end of file diff --git a/hawkbit-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactStore.java b/hawkbit-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactStore.java new file mode 100755 index 000000000..c5cf03016 --- /dev/null +++ b/hawkbit-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactStore.java @@ -0,0 +1,280 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.artifact.repository; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.gridfs.GridFsOperations; + +import com.google.common.io.BaseEncoding; +import com.google.common.io.ByteStreams; +import com.mongodb.BasicDBObject; +import com.mongodb.MongoClientException; +import com.mongodb.MongoException; +import com.mongodb.gridfs.GridFSDBFile; +import com.mongodb.gridfs.GridFSFile; + +/** + * The file management which looks up all the file in the filestore. + * + * + * + * + */ +public class ArtifactStore implements ArtifactRepository { + + private static final Logger LOGGER = LoggerFactory.getLogger(ArtifactStore.class); + + /** + * The mongoDB field which holds the filename of the file to download. SP + * Server uses the SHA hash as a filename and lookup in the mongoDB. + */ + private static final String FILENAME = "filename"; + + /** + * The mongoDB field which automatically calculated by the mongoDB. + */ + private static final String MD5 = "md5"; + + /** + * The mongoDB field which holds the SHA1 hash, stored in the metadata + * object. + */ + private static final String SHA1 = "sha1"; + + private static final String ID = "_id"; + + @Autowired + private GridFsOperations gridFs; + + MongoTemplate mongoTemplate; + + /** + * Retrieves a {@link GridFSDBFile} from the store by it's SHA1 hash. + * + * @param tenant + * the tenant to retrieve the artifacts from, ignore case. + * @param sha1Hash + * the sha1-hash of the file to lookup. + * @return The gridfs file object or {@code null} if no file exists. + */ + @Override + public DbArtifact getArtifactBySha1(final String sha1Hash) { + return map(gridFs.findOne(new Query().addCriteria(Criteria.where(FILENAME).is(sha1Hash)))); + } + + /** + * Retrieves a {@link GridFSDBFile} from the store by it's MD5 hash. + * + * @param tenant + * the tenant to retrieve the artifacts from, ignore case. + * @param md5Hash + * the md5-hash of the file to lookup. + * @return The gridfs file object or {@code null} if no file exists. + */ + public DbArtifact getArtifactByMd5(final String md5Hash) { + return map(gridFs.findOne(new Query().addCriteria(Criteria.where(MD5).is(md5Hash)))); + } + + /** + * Retrieves a {@link GridFSDBFile} from the store by it's object id. + * + * @param tenant + * the tenant to retrieve the artifacts from, ignore case. + * @param id + * the id of the file to lookup. + * @return The gridfs file object or {@code null} if no file exists. + */ + @Override + public DbArtifact getArtifactById(final String id) { + return map(gridFs.findOne(new Query().addCriteria(Criteria.where(ID).is(id)))); + } + + @Override + public DbArtifact store(final InputStream content, final String filename, final String contentType) { + return store(content, filename, contentType, null); + } + + @Override + public DbArtifact store(final InputStream content, final String filename, final String contentType, + final DbArtifactHash hash) { + File tempFile = null; + try { + LOGGER.debug("storing file {} of content {}", filename, contentType); + tempFile = File.createTempFile("uploadFile", null); + try (final FileOutputStream os = new FileOutputStream(tempFile)) { + return store(content, contentType, os, tempFile, hash); + } + } catch (final IOException | MongoException e1) { + throw new ArtifactStoreException(e1.getMessage(), e1); + } finally { + if (tempFile != null && !tempFile.delete()) { + LOGGER.error("Could not delete temporary file: {}", tempFile); + } + } + } + + @Override + public void deleteById(final String artifactId) { + try { + deleteArtifact(gridFs.findOne(new Query().addCriteria(Criteria.where(ID).is(artifactId)))); + } catch (final MongoException e) { + throw new ArtifactStoreException(e.getMessage(), e); + } + } + + @Override + public void deleteBySha1(final String sha1Hash) { + try { + deleteArtifact(gridFs.findOne(new Query().addCriteria(Criteria.where(FILENAME).is(sha1Hash)))); + } catch (final MongoException e) { + throw new ArtifactStoreException(e.getMessage(), e); + } + } + + private void deleteArtifact(final GridFSDBFile dbFile) { + if (dbFile != null) { + try { + gridFs.delete(new Query().addCriteria(Criteria.where(ID).is(dbFile.getId()))); + } catch (final MongoClientException e) { + throw new ArtifactStoreException(e.getMessage(), e); + } + } + + } + + private DbArtifact store(final InputStream content, final String contentType, final FileOutputStream os, + final File tempFile, final DbArtifactHash hash) { + final GridFsArtifact storedArtifact; + try { + final String sha1Hash = computeSHA1Hash(content, os, hash != null ? hash.getSha1() : null); + // upload if it does not exist already, check if file exists, not + // tenant specific. + final GridFSDBFile result = gridFs.findOne(new Query().addCriteria(Criteria.where(FILENAME).is(sha1Hash))); + if (null == result) { + try (FileInputStream inputStream = new FileInputStream(tempFile)) { + final BasicDBObject metadata = new BasicDBObject(); + metadata.put(SHA1, sha1Hash); + storedArtifact = map(gridFs.store(inputStream, sha1Hash, contentType, metadata)); + } + } else { + LOGGER.info("file with sha1 hash {} already exists in database, increase reference counter", sha1Hash); + result.save(); + storedArtifact = map(result); + } + } catch (final NoSuchAlgorithmException | IOException e) { + throw new ArtifactStoreException(e.getMessage(), e); + } + + if (hash != null && hash.getMd5() != null && !storedArtifact.getHashes().getMd5().equals(hash.getMd5())) { + throw new HashNotMatchException("The given md5 hash " + hash.getMd5() + + " not matching the calculated md5 hash " + storedArtifact.getHashes().getMd5(), + HashNotMatchException.MD5); + } + + return storedArtifact; + + } + + private String computeSHA1Hash(final InputStream stream, final FileOutputStream os, final String providedSHA1Sum) + throws NoSuchAlgorithmException, IOException { + String sha1Hash; + // compute digest + final MessageDigest md = MessageDigest.getInstance("SHA-1"); + final DigestOutputStream dos = new DigestOutputStream(os, md); + ByteStreams.copy(stream, dos); + dos.close(); + sha1Hash = BaseEncoding.base16().lowerCase().encode(md.digest()); + if (providedSHA1Sum != null && !providedSHA1Sum.equalsIgnoreCase(sha1Hash)) { + throw new HashNotMatchException( + "The given sha1 hash " + providedSHA1Sum + " not matching the calculated sha1 hash " + sha1Hash, + HashNotMatchException.SHA1); + } + return sha1Hash; + } + + /** + * Maps a list of {@link GridFSDBFile} to paged list of {@link DbArtifact}s. + * + * @param tenant + * the tenant + * @param dbFiles + * the list of mongoDB gridFs files. + * @return a paged list of artifacts mapped from the given dbFiles + */ + private List map(final List dbFiles) { + final List collect = dbFiles.stream().map(dbFile -> map(dbFile)).collect(Collectors.toList()); + return collect; + } + + /** + * Retrieves a list of {@link GridFSDBFile} from the store by all SHA1 + * hashes. + * + * @param tenant + * the tenant to retrieve the artifacts from, ignore case. + * @param sha1Hashes + * the sha1-hashes of the files to lookup. + * @return list of artfiacts + */ + public List getArtifactsBySha1(final List sha1Hashes) { + return map(gridFs.find(new Query().addCriteria(Criteria.where(SHA1).in(sha1Hashes)))); + } + + /** + * Retrieves a list of {@link GridFSDBFile} from the store by all ids. + * + * @param tenant + * the tenant to retrieve the artifacts from, ignore case. + * @param ids + * the ids of the files to lookup. + * @return list of artfiacts + */ + public List getArtifactsByIds(final List ids) { + return map(gridFs.find(new Query().addCriteria(Criteria.where(ID).in(ids)))); + } + + /** + * Maps a single {@link GridFSFile} to {@link DbArtifact}. + * + * @param tenant + * the tenant + * @param dbFile + * the mongoDB gridFs file. + * @return a mapped artifact from the given dbFile + */ + private GridFsArtifact map(final GridFSFile fsFile) { + if (fsFile == null) { + return null; + } + final GridFsArtifact artifact = new GridFsArtifact(fsFile); + artifact.setArtifactId(fsFile.getId().toString()); + artifact.setSize(fsFile.getLength()); + artifact.setContentType(fsFile.getContentType()); + artifact.setHashes(new DbArtifactHash(fsFile.getFilename(), fsFile.getMD5())); + return artifact; + } +} diff --git a/hawkbit-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactStoreAutoConfiguration.java b/hawkbit-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactStoreAutoConfiguration.java new file mode 100755 index 000000000..43bcddaf0 --- /dev/null +++ b/hawkbit-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactStoreAutoConfiguration.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.artifact.repository; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * Auto configuration for the {@link ArtifactStore}. + * + * + * + */ +@Configuration +@ComponentScan +@ConditionalOnMissingBean(value = ArtifactRepository.class) +@Import(value = MongoConfiguration.class) +public class ArtifactStoreAutoConfiguration { + + /** + * @return Default {@link ArtifactRepository} implementation. + */ + @Bean + public ArtifactRepository artifactRepository() { + return new ArtifactStore(); + } +} diff --git a/hawkbit-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/GridFsArtifact.java b/hawkbit-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/GridFsArtifact.java new file mode 100755 index 000000000..fc3461d9a --- /dev/null +++ b/hawkbit-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/GridFsArtifact.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.artifact.repository; + +import java.io.InputStream; + +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; + +import com.mongodb.gridfs.GridFSDBFile; +import com.mongodb.gridfs.GridFSFile; + +/** + * A wrapper object for the {@link DbArtifact} object which returns the + * {@link InputStream} directly from {@link GridFSDBFile#getInputStream()} which + * retrieves when calling {@link #getFileInputStream()} always a new + * {@link InputStream} and not the same. + * + * + * + */ +public class GridFsArtifact extends DbArtifact { + + private final GridFSFile dbFile; + + /** + * @param dbFile + */ + public GridFsArtifact(final GridFSFile dbFile) { + this.dbFile = dbFile; + } + + @Override + public InputStream getFileInputStream() { + if (dbFile instanceof GridFSDBFile) { + return ((GridFSDBFile) dbFile).getInputStream(); + } + return null; + } +} diff --git a/hawkbit-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/MongoConfiguration.java b/hawkbit-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/MongoConfiguration.java new file mode 100755 index 000000000..602af6f54 --- /dev/null +++ b/hawkbit-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/MongoConfiguration.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.artifact.repository; + +import java.net.UnknownHostException; +import java.util.Arrays; + +import javax.annotation.PreDestroy; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.mongo.MongoProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.data.mongodb.config.AbstractMongoConfiguration; + +import com.mongodb.Mongo; +import com.mongodb.MongoClient; +import com.mongodb.MongoClientOptions; +import com.mongodb.MongoClientOptions.Builder; +import com.mongodb.MongoClientURI; +import com.mongodb.ServerAddress; + +/** + * {@link AbstractMongoConfiguration} that uses {@link MongoClientURI} even when + * port is configured for NON {@link Cloud} use cases. + * + */ +@Configuration +@EnableConfigurationProperties(MongoProperties.class) +@ConditionalOnClass(Mongo.class) +@ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDbFactory") +@Profile({ "!cloud" }) +public class MongoConfiguration extends AbstractMongoConfiguration { + private static final Logger LOG = LoggerFactory.getLogger(MongoConfiguration.class); + + @Autowired + private MongoProperties properties; + + @Autowired(required = false) + private MongoClientOptions options; + + private Mongo mongo; + + @Override + public String getDatabaseName() { + return properties.getMongoClientDatabase(); + } + + /** + * Closes mongo client when destroyd. + */ + @PreDestroy + public void close() { + if (this.mongo != null) { + this.mongo.close(); + } + } + + @Override + @Bean + @ConditionalOnMissingBean + public Mongo mongo() throws UnknownHostException { + final MongoClientURI uri = new MongoClientURI(properties.getUri(), createBuilderOutOfOptions(options)); + + if (properties.getPort() != null) { + LOG.debug("Create mongo by properties (host: {}, port: {})", uri.getHosts().get(0), properties.getPort()); + this.mongo = new MongoClient(Arrays.asList(new ServerAddress(uri.getHosts().get(0), properties.getPort())), + uri.getOptions()); + } else { + LOG.debug("Create mongo by URI : {}", uri); + this.mongo = new MongoClient(uri); + } + + return this.mongo; + } + + /* + * Creates {@link MongoClientOptions} builder out of existing options as the + * {@link MongoClientURI} expects a builder. + * + * Based on MongoProperties#builder method. + */ + private Builder createBuilderOutOfOptions(final MongoClientOptions options) { + final Builder builder = MongoClientOptions.builder(); + if (options != null) { + builder.alwaysUseMBeans(options.isAlwaysUseMBeans()); + builder.connectionsPerHost(options.getConnectionsPerHost()); + builder.connectTimeout(options.getConnectTimeout()); + builder.cursorFinalizerEnabled(options.isCursorFinalizerEnabled()); + builder.dbDecoderFactory(options.getDbDecoderFactory()); + builder.dbEncoderFactory(options.getDbEncoderFactory()); + builder.description(options.getDescription()); + builder.maxWaitTime(options.getMaxWaitTime()); + builder.readPreference(options.getReadPreference()); + builder.serverSelectionTimeout(options.getServerSelectionTimeout()); + builder.socketFactory(options.getSocketFactory()); + builder.socketKeepAlive(options.isSocketKeepAlive()); + builder.socketTimeout(options.getSocketTimeout()); + builder.threadsAllowedToBlockForConnectionMultiplier(options + .getThreadsAllowedToBlockForConnectionMultiplier()); + builder.writeConcern(options.getWriteConcern()); + } + return builder; + } +} diff --git a/hawkbit-artifact-repository-mongo/src/main/resources/META-INF/spring.factories b/hawkbit-artifact-repository-mongo/src/main/resources/META-INF/spring.factories new file mode 100755 index 000000000..35e5415c9 --- /dev/null +++ b/hawkbit-artifact-repository-mongo/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configure +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.eclipse.hawkbit.artifact.repository.ArtifactStoreAutoConfiguration \ No newline at end of file diff --git a/hawkbit-artifact-repository-mongo/src/test/java/org/eclipse/hawkbit/artifact/FreePortFileWriter.java b/hawkbit-artifact-repository-mongo/src/test/java/org/eclipse/hawkbit/artifact/FreePortFileWriter.java new file mode 100755 index 000000000..257c85076 --- /dev/null +++ b/hawkbit-artifact-repository-mongo/src/test/java/org/eclipse/hawkbit/artifact/FreePortFileWriter.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.artifact; + +import java.io.File; +import java.net.InetSocketAddress; +import java.net.ServerSocket; + +/** + * + * + */ +public class FreePortFileWriter { + + private final String filePortPath; + private final int from; + private final int to; + + /** + * @param from + * @param to + */ + public FreePortFileWriter(final int from, final int to, final String filePortPath) { + this.from = from; + this.to = to; + this.filePortPath = filePortPath; + } + + public int getPort() { + return findFree(); + } + + protected int findFree() { + for (int i = from; i <= to; i++) { + if (isFree(i)) { + return i; + } + } + throw new RuntimeException("No free port in range " + from + ":" + to); + } + + boolean isFree(final int port) { + try { + final File portFile = new File(filePortPath + File.separator + port + ".port"); + portFile.getParentFile().mkdirs(); + if (portFile.exists()) { + return false; + } else { + boolean isFree = false; + final ServerSocket sock = new ServerSocket(); + sock.setReuseAddress(true); + sock.bind(new InetSocketAddress(port)); + if (portFile.createNewFile()) { + portFile.deleteOnExit(); + isFree = true; + } + sock.close(); + // is free: + return isFree; + // We rely on an exception thrown to determine availability or + // not availability. + } + } catch (final Exception e) { + // not free. + return false; + } + } + +} diff --git a/hawkbit-artifact-repository-mongo/src/test/java/org/eclipse/hawkbit/artifact/MongoDBTestRule.java b/hawkbit-artifact-repository-mongo/src/test/java/org/eclipse/hawkbit/artifact/MongoDBTestRule.java new file mode 100755 index 000000000..d8485787e --- /dev/null +++ b/hawkbit-artifact-repository-mongo/src/test/java/org/eclipse/hawkbit/artifact/MongoDBTestRule.java @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.artifact; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.util.UUID; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.flapdoodle.embed.mongo.Command; +import de.flapdoodle.embed.mongo.MongodExecutable; +import de.flapdoodle.embed.mongo.MongodProcess; +import de.flapdoodle.embed.mongo.MongodStarter; +import de.flapdoodle.embed.mongo.config.ArtifactStoreBuilder; +import de.flapdoodle.embed.mongo.config.DownloadConfigBuilder; +import de.flapdoodle.embed.mongo.config.IMongodConfig; +import de.flapdoodle.embed.mongo.config.MongodConfigBuilder; +import de.flapdoodle.embed.mongo.config.Net; +import de.flapdoodle.embed.mongo.config.RuntimeConfigBuilder; +import de.flapdoodle.embed.mongo.distribution.Version; +import de.flapdoodle.embed.process.config.store.HttpProxyFactory; +import de.flapdoodle.embed.process.runtime.Network; + +/** + * + * + */ +public class MongoDBTestRule implements TestRule { + + private static final Logger LOG = LoggerFactory.getLogger(MongoDBTestRule.class); + private static volatile MongodExecutable mongodExecutable = null; + private static volatile MongodProcess mongod; + private final String id = UUID.randomUUID().toString(); + + @Override + public Statement apply(final Statement base, final Description description) { + return statement(base, description); + } + + private Statement statement(final Statement base, final Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + before(base, description); + try { + base.evaluate(); + } finally { + after(); + } + } + }; + } + + private void after() { + if (mongodExecutable != null) { + LOG.info("Stop MongoDB..."); + mongodExecutable.stop(); + mongodExecutable = null; + if (mongod != null) { + mongod.stop(); + mongod = null; + } + LOG.info("MongoDB stopped... {}", id); + } + } + + private void before(final Statement base, final Description description) throws UnknownHostException, IOException { + final Command command = Command.MongoD; + + final RuntimeConfigBuilder runtimeConfig = new RuntimeConfigBuilder().defaults(command); + + int port = -1; + if (System.getProperty("spring.data.mongodb.port") != null) { + port = Integer.parseInt(System.getProperty("spring.data.mongodb.port")); + } else { + port = new FreePortFileWriter(27017, 27100, "./target/freeports").getPort(); + System.setProperty("spring.data.mongodb.port", String.valueOf(port)); + } + + Version version = Version.V3_1_0; + if (System.getProperty("inf.mongodb.version") != null) { + version = Version.valueOf("V" + System.getProperty("inf.mongodb.version").trim().replaceAll("\\.", "_")); + } + + if (System.getProperty("http.proxyHost") != null) { + runtimeConfig.artifactStore(new ArtifactStoreBuilder().defaults(command) + .download(new DownloadConfigBuilder().defaultsForCommand(command) + .proxyFactory(new HttpProxyFactory(System.getProperty("http.proxyHost").trim(), + Integer.valueOf(System.getProperty("http.proxyPort")))))); + } + + final IMongodConfig mongodConfig = new MongodConfigBuilder().version(version) + .net(new Net("127.0.0.1", port, Network.localhostIsIPv6())).build(); + + final MongodStarter starter = MongodStarter.getInstance(runtimeConfig.build()); + mongodExecutable = starter.prepare(mongodConfig); + LOG.info("Start MongoDB..."); + mongod = mongodExecutable.start(); + + final Net net = mongod.getConfig().net(); + LOG.info("MongoDB started id {} on bind ip :{} Port:{} and version {}", id, net.getBindIp(), net.getPort(), + mongodConfig.version().toString()); + } +} diff --git a/hawkbit-artifact-repository-mongo/src/test/java/org/eclipse/hawkbit/artifact/TestConfiguration.java b/hawkbit-artifact-repository-mongo/src/test/java/org/eclipse/hawkbit/artifact/TestConfiguration.java new file mode 100755 index 000000000..d59050e29 --- /dev/null +++ b/hawkbit-artifact-repository-mongo/src/test/java/org/eclipse/hawkbit/artifact/TestConfiguration.java @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.artifact; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; + +@ComponentScan +@EnableAutoConfiguration +public class TestConfiguration { +} diff --git a/hawkbit-artifact-repository-mongo/src/test/java/org/eclipse/hawkbit/artifact/repository/ArtifactStoreTest.java b/hawkbit-artifact-repository-mongo/src/test/java/org/eclipse/hawkbit/artifact/repository/ArtifactStoreTest.java new file mode 100755 index 000000000..2a8441ee6 --- /dev/null +++ b/hawkbit-artifact-repository-mongo/src/test/java/org/eclipse/hawkbit/artifact/repository/ArtifactStoreTest.java @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.artifact.repository; + +import static org.fest.assertions.api.Assertions.assertThat; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Random; + +import org.apache.commons.io.IOUtils; +import org.eclipse.hawkbit.artifact.MongoDBTestRule; +import org.eclipse.hawkbit.artifact.TestConfiguration; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.gridfs.GridFsOperations; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.google.common.io.BaseEncoding; +import com.mongodb.gridfs.GridFSDBFile; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = { ArtifactStoreAutoConfiguration.class, TestConfiguration.class }) +public class ArtifactStoreTest { + + @ClassRule + public static final MongoDBTestRule mongoDBRule = new MongoDBTestRule(); + + @Autowired + private ArtifactStore artifactStoreUnderTest; + + @Autowired + private GridFsOperations gridFs; + + @Test + public void storeArtifactInMongoDB() { + final int filelengthBytes = 128; + final String filename = "testfile.json"; + final String contentType = "application/json"; + final DbArtifact storedFile = artifactStoreUnderTest.store(generateInputStream(filelengthBytes), filename, + contentType); + + assertThat(storedFile).isNotNull(); + assertThat(artifactStoreUnderTest.getArtifactById(storedFile.getArtifactId())).isNotNull(); + } + + @Test + public void findArtifactBySHA1Hash() throws NoSuchAlgorithmException { + final int filelengthBytes = 128; + final String filename = "testfile.json"; + final String contentType = "application/json"; + + final DigestInputStream digestInputStream = digestInputStream(generateInputStream(filelengthBytes), "SHA-1"); + artifactStoreUnderTest.store(digestInputStream, filename, contentType); + assertThat(artifactStoreUnderTest.getArtifactBySha1( + BaseEncoding.base16().lowerCase().encode(digestInputStream.getMessageDigest().digest()))).isNotNull(); + } + + @Test + public void findArtifactByMD5Hash() throws NoSuchAlgorithmException { + final int filelengthBytes = 128; + final String filename = "testfile.json"; + final String contentType = "application/json"; + + final DigestInputStream digestInputStream = digestInputStream(generateInputStream(filelengthBytes), "MD5"); + artifactStoreUnderTest.store(digestInputStream, filename, contentType); + assertThat(artifactStoreUnderTest.getArtifactByMd5( + BaseEncoding.base16().lowerCase().encode(digestInputStream.getMessageDigest().digest()))).isNotNull(); + } + + @Test + public void getInputStreamFromArtifact() throws IOException { + final int filelengthBytes = 128; + final String filename = "testfile.json"; + final String contentType = "application/json"; + + final ByteArrayInputStream inputStream = generateInputStream(filelengthBytes); + final DbArtifact artifact = artifactStoreUnderTest + .getArtifactById(artifactStoreUnderTest.store(inputStream, filename, contentType).getArtifactId()); + inputStream.reset(); + + final byte[] artifactBytes = new byte[filelengthBytes]; + final byte[] artifactStoredBytes = new byte[filelengthBytes]; + IOUtils.readFully(inputStream, artifactBytes); + IOUtils.readFully(artifact.getFileInputStream(), artifactStoredBytes); + + assertThat(artifactBytes).isEqualTo(artifactStoredBytes); + } + + @Test + public void deleteArtifactWithOnlyOneTenantLast() throws NoSuchAlgorithmException { + final int filelengthBytes = 128; + final String filename = "testfile.json"; + final String contentType = "application/json"; + + final DigestInputStream digestInputStream = digestInputStream(generateInputStream(filelengthBytes), "SHA-1"); + final DbArtifact store = artifactStoreUnderTest.store(digestInputStream, filename, contentType); + + artifactStoreUnderTest.deleteById(store.getArtifactId()); + + final GridFSDBFile findOne = gridFs + .findOne(new Query().addCriteria(Criteria.where("_id").is(store.getArtifactId()))); + assertThat(findOne).isNull(); + } + + private static ByteArrayInputStream generateInputStream(final int length) { + final byte[] bytes = new byte[length]; + new Random().nextBytes(bytes); + return new ByteArrayInputStream(bytes); + } + + private static DigestInputStream digestInputStream(final ByteArrayInputStream stream, final String digest) + throws NoSuchAlgorithmException { + return new DigestInputStream(stream, MessageDigest.getInstance(digest)); + } + +} diff --git a/hawkbit-autoconfigure/pom.xml b/hawkbit-autoconfigure/pom.xml new file mode 100755 index 000000000..73e93e830 --- /dev/null +++ b/hawkbit-autoconfigure/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + org.eclipse.hawkbit + hawkbit-parent + 0.1.0-SNAPSHOT + + hawkbit-autoconfigure + hawkBit :: Spring Boot Autoconfigure + + + + org.eclipse.hawkbit + hawkbit-core + ${project.version} + true + + + org.eclipse.hawkbit + hawkbit-cache-redis + ${project.version} + true + + + org.eclipse.hawkbit + hawkbit-dmf-amqp + ${project.version} + true + + + org.eclipse.hawkbit + hawkbit-ui + ${project.version} + true + + + org.eclipse.hawkbit + hawkbit-repository + ${project.version} + true + + + org.eclipse.hawkbit + hawkbit-http-security + ${project.version} + true + + + org.eclipse.hawkbit + hawkbit-security-core + ${project.version} + true + + + org.eclipse.hawkbit + hawkbit-rest-resource + ${project.version} + true + + + io.springfox + springfox-swagger2 + true + + + org.springframework + spring-context-support + + + diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/amqp/AmqpAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/amqp/AmqpAutoConfiguration.java new file mode 100755 index 000000000..681e485fc --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/amqp/AmqpAutoConfiguration.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.amqp; + +import org.eclipse.hawkbit.amqp.AmqpConfiguration; +import org.eclipse.hawkbit.amqp.annotation.EnableAmqp; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Configuration; + +/** + * The amqp autoconfiguration. + * + * + * + */ +@Configuration +@ConditionalOnClass(value = AmqpConfiguration.class) +@EnableAmqp +public class AmqpAutoConfiguration { + +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/CacheAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/CacheAutoConfiguration.java new file mode 100755 index 000000000..6696c44ca --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/CacheAutoConfiguration.java @@ -0,0 +1,197 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.cache; + +import java.util.Collection; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.cache.TenancyCacheManager; +import org.eclipse.hawkbit.cache.TenantAwareCacheManager; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.guava.GuavaCacheManager; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.SimpleCacheResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * A configuration for configuring the spring {@link CacheManager} for specific + * multi-tenancy caching. The caches between tenants must not interfere each + * other. + * + * This is done by providing a special {@link TenantCacheResolver} which + * generates a cache name included the current tenant. + * + * + * + */ +@Configuration +@EnableCaching +public class CacheAutoConfiguration extends CachingConfigurerSupport { + + @Autowired + private TenantAware tenantAware; + + /** + * @return the default cache manager bean if none other cache manager is + * existing. + */ + @Override + @Bean + @ConditionalOnMissingBean + public TenancyCacheManager cacheManager() { + return new TenantAwareCacheManager(new GuavaCacheManager(), tenantAware); + } + + /** + * A {@link SimpleCacheResolver} implementation which includes the + * {@link TenantAware#getCurrentTenant()} into the cache name before + * resolving it. + * + * + * + * + */ + public class TenantCacheResolver extends SimpleCacheResolver { + + /* + * (non-Javadoc) + * + * @see org.springframework.cache.interceptor.AbstractCacheResolver# + * resolveCaches(org.springframework + * .cache.interceptor.CacheOperationInvocationContext) + */ + @Override + public Collection resolveCaches(final CacheOperationInvocationContext context) { + return super.resolveCaches(context).stream().map(cache -> new TenantCacheWrapper(cache)) + .collect(Collectors.toList()); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.cache.interceptor.SimpleCacheResolver# + * getCacheNames(org.springframework + * .cache.interceptor.CacheOperationInvocationContext) + */ + @Override + protected Collection getCacheNames(final CacheOperationInvocationContext context) { + return super.getCacheNames(context).stream() + .map(cacheName -> tenantAware.getCurrentTenant() + "." + cacheName).collect(Collectors.toList()); + } + } + + /** + * An {@link Cache} wrapper which returns the name of the cache include the + * {@link TenantAware#getCurrentTenant()}. + * + * + * + * + */ + public class TenantCacheWrapper implements Cache { + private final Cache delegate; + + /** + * @param delegate + */ + public TenantCacheWrapper(final Cache delegate) { + this.delegate = delegate; + } + + /** + * @return + * @see org.springframework.cache.Cache#getName() + */ + @Override + public String getName() { + return tenantAware.getCurrentTenant() + "." + delegate.getName(); + } + + /** + * @return + * @see org.springframework.cache.Cache#getNativeCache() + */ + @Override + public Object getNativeCache() { + return delegate.getNativeCache(); + } + + /** + * @param key + * @return + * @see org.springframework.cache.Cache#get(java.lang.Object) + */ + @Override + public ValueWrapper get(final Object key) { + return delegate.get(key); + } + + /** + * @param key + * @param type + * @return + * @see org.springframework.cache.Cache#get(java.lang.Object, + * java.lang.Class) + */ + @Override + public T get(final Object key, final Class type) { + return delegate.get(key, type); + } + + /** + * @param key + * @param value + * @see org.springframework.cache.Cache#put(java.lang.Object, + * java.lang.Object) + */ + @Override + public void put(final Object key, final Object value) { + delegate.put(key, value); + } + + /** + * @param key + * @param value + * @return + * @see org.springframework.cache.Cache#putIfAbsent(java.lang.Object, + * java.lang.Object) + */ + @Override + public ValueWrapper putIfAbsent(final Object key, final Object value) { + return delegate.putIfAbsent(key, value); + } + + /** + * @param key + * @see org.springframework.cache.Cache#evict(java.lang.Object) + */ + @Override + public void evict(final Object key) { + delegate.evict(key); + } + + /** + * + * @see org.springframework.cache.Cache#clear() + */ + @Override + public void clear() { + delegate.clear(); + } + + } + +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/DownloadIdCacheAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/DownloadIdCacheAutoConfiguration.java new file mode 100755 index 000000000..495200dec --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/DownloadIdCacheAutoConfiguration.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.cache; + +import org.eclipse.hawkbit.cache.CacheConstants; +import org.eclipse.hawkbit.cache.TenancyCacheManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * A configuration for configuring a cache for the download id's. + * + * This is done by providing a named cache. + * + * + * + */ +@Configuration +public class DownloadIdCacheAutoConfiguration { + + @Autowired + private CacheManager cacheManager; + + /** + * Bean for the downlod id cache. + * + * @return the cache + */ + @Bean(name = CacheConstants.DOWNLOAD_ID_CACHE) + public Cache downloadIdCache() { + if (cacheManager instanceof TenancyCacheManager) { + return ((TenancyCacheManager) cacheManager).getDirectCache(CacheConstants.DOWNLOAD_ID_CACHE); + } + return cacheManager.getCache(CacheConstants.DOWNLOAD_ID_CACHE); + } + +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/RedisAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/RedisAutoConfiguration.java new file mode 100755 index 000000000..a6c1062e3 --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/RedisAutoConfiguration.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.cache; + +import org.eclipse.hawkbit.cache.RedisConfiguration; +import org.eclipse.hawkbit.cache.annotation.EnableRedis; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Configuration; + +/** + * A configuration for configuring the redis configuration. + * + * + * + */ +@Configuration +@ConditionalOnClass(value = RedisConfiguration.class) +@EnableRedis +public class RedisAutoConfiguration { + +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/conf/ControllerPollAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/conf/ControllerPollAutoConfiguration.java new file mode 100755 index 000000000..bcbc5ec16 --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/conf/ControllerPollAutoConfiguration.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.conf; + +import org.eclipse.hawkbit.ControllerPollProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * Enable the Controlle Poll. + * + * + * + */ +@Configuration +@ConditionalOnClass(ControllerPollProperties.class) +@EnableConfigurationProperties(ControllerPollProperties.class) +public class ControllerPollAutoConfiguration { + +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/conf/DefaultPropertiesAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/conf/DefaultPropertiesAutoConfiguration.java new file mode 100755 index 000000000..deb8763b9 --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/conf/DefaultPropertiesAutoConfiguration.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.conf; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +/** + * Autoconfiguration which loads the default configuration properties for + * hawkbit. + * + * + * + */ +@Configuration +@PropertySource("classpath:/hawkbitdefaults.properties") +@Order(Ordered.HIGHEST_PRECEDENCE) +public class DefaultPropertiesAutoConfiguration { + +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/eventbus/EventBusAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/eventbus/EventBusAutoConfiguration.java new file mode 100755 index 000000000..e09c0a68e --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/eventbus/EventBusAutoConfiguration.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.eventbus; + +import java.util.concurrent.Executor; + +import org.eclipse.hawkbit.eventbus.EventBusSubscriberProcessor; +import org.eclipse.hawkbit.eventbus.EventSubscriber; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.google.common.eventbus.AsyncEventBus; +import com.google.common.eventbus.EventBus; + +/** + * + * + */ +@Configuration +public class EventBusAutoConfiguration { + + @Autowired + @Qualifier("asyncExecutor") + private Executor executor; + + /** + * Server internal eventBus that allows parallel event processing if the + * subscriber is marked as so. + * + * @return eventbus bean + */ + @Bean + @ConditionalOnMissingBean + public EventBus eventBus() { + return new AsyncEventBus(executor); + } + + /** + * @return the {@link EventBusSubscriberProcessor} to find classes annotated + * with {@link EventSubscriber}. + */ + @Bean + @ConditionalOnMissingBean + public EventBusSubscriberProcessor eventBusSubscriberProcessor() { + return new EventBusSubscriberProcessor(); + } + +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/scheduling/AsyncConfigurerAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/scheduling/AsyncConfigurerAutoConfiguration.java new file mode 100755 index 000000000..cdbbcba5b --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/scheduling/AsyncConfigurerAutoConfiguration.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.scheduling; + +import java.util.concurrent.Executor; + +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; + +/** + * + * + */ + +@Configuration +@EnableAsync +@ConditionalOnMissingBean(AsyncConfigurer.class) +public class AsyncConfigurerAutoConfiguration implements AsyncConfigurer { + + @Autowired + @Qualifier("asyncExecutor") + private Executor executor; + + @Override + public Executor getAsyncExecutor() { + return executor; + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new SimpleAsyncUncaughtExceptionHandler(); + } + +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/scheduling/AsyncConfigurerThreadpoolProperties.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/scheduling/AsyncConfigurerThreadpoolProperties.java new file mode 100755 index 000000000..f7f18f1b0 --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/scheduling/AsyncConfigurerThreadpoolProperties.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.scheduling; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Properties for the async configurer. + * + * + */ +@ConfigurationProperties("hawkbit.threadpool") +public class AsyncConfigurerThreadpoolProperties { + + private Integer queuesize = 250; + + private Integer corethreads = 5; + + private Integer maxthreads = 50; + + private Long idletimeout = 10000L; + + public Integer getQueuesize() { + return queuesize; + } + + public void setQueuesize(final Integer queuesize) { + this.queuesize = queuesize; + } + + public Integer getCorethreads() { + return corethreads; + } + + public void setCorethreads(final Integer corethreads) { + this.corethreads = corethreads; + } + + public Integer getMaxthreads() { + return maxthreads; + } + + public void setMaxthreads(final Integer maxthreads) { + this.maxthreads = maxthreads; + } + + public Long getIdletimeout() { + return idletimeout; + } + + public void setIdletimeout(final Long idletimeout) { + this.idletimeout = idletimeout; + } + +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/scheduling/ExecutorAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/scheduling/ExecutorAutoConfiguration.java new file mode 100755 index 000000000..f626e94b5 --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/scheduling/ExecutorAutoConfiguration.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.scheduling; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.concurrent.DelegatingSecurityContextExecutor; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +/** + * + * + */ + +@Configuration +@EnableConfigurationProperties(AsyncConfigurerThreadpoolProperties.class) +public class ExecutorAutoConfiguration { + + private static final Logger LOGGER = LoggerFactory.getLogger(ExecutorAutoConfiguration.class); + + @Autowired + private AsyncConfigurerThreadpoolProperties asyncConfigurerProperties; + + /** + * @return ExecutorService for general pupose multi threaded operations + */ + @Bean + @ConditionalOnMissingBean + public Executor asyncExecutor() { + final BlockingQueue blockingQueue = new ArrayBlockingQueue( + asyncConfigurerProperties.getQueuesize()); + final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( + asyncConfigurerProperties.getCorethreads(), asyncConfigurerProperties.getMaxthreads(), + asyncConfigurerProperties.getIdletimeout(), TimeUnit.MILLISECONDS, blockingQueue, + new ThreadFactoryBuilder().setNameFormat("central-executor-pool-%d").build()); + threadPoolExecutor.setRejectedExecutionHandler(new RejectedExecutionHandler() { + @Override + public void rejectedExecution(final Runnable r, final java.util.concurrent.ThreadPoolExecutor executor) { + LOGGER.warn("Reject runnable for centralExecutorService, reached limit of queue size {}", executor + .getQueue().size()); + } + }); + return new DelegatingSecurityContextExecutor(threadPoolExecutor); + } + + /** + * @return the executor for UI background processes. + */ + @Bean(name = "uiExecutor") + @ConditionalOnMissingBean(name = "uiExecutor") + public Executor uiExecutor() { + final BlockingQueue blockingQueue = new ArrayBlockingQueue<>(20); + final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 20, 10000, TimeUnit.MILLISECONDS, + blockingQueue, new ThreadFactoryBuilder().setNameFormat("ui-executor-pool-%d").build()); + threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + return new DelegatingSecurityContextExecutor(threadPoolExecutor); + } + +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/EnableHawkbitManagedSecurityConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/EnableHawkbitManagedSecurityConfiguration.java new file mode 100755 index 000000000..ad8b48dc3 --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/EnableHawkbitManagedSecurityConfiguration.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.security; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; + +/** + * Annotation to enable the managed security configuration. + * + * + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(SecurityManagedConfiguration.class) +public @interface EnableHawkbitManagedSecurityConfiguration { + +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityAutoConfiguration.java new file mode 100755 index 000000000..326ff4774 --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityAutoConfiguration.java @@ -0,0 +1,206 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.security; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.hawkbit.im.authentication.MultitenancyIndicator; +import org.eclipse.hawkbit.im.authentication.PermissionService; +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails; +import org.eclipse.hawkbit.im.authentication.UserAuthenticationFilter; +import org.eclipse.hawkbit.security.SecurityContextTenantAware; +import org.eclipse.hawkbit.security.SecurityProperties; +import org.eclipse.hawkbit.security.SpringSecurityAuditorAware; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.AuditorAware; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for security. + * + * + * + */ +@Configuration +@EnableConfigurationProperties(SecurityProperties.class) +public class SecurityAutoConfiguration { + + /** + * @return the {@link TenantAware} singleton bean which holds the current + * {@link TenantAware} service and make it accessible in beans which + * cannot access the service directly, e.g. JPA entities. + */ + @Bean + @ConditionalOnMissingBean + public TenantAware tenantAware() { + return new SecurityContextTenantAware(); + } + + /** + * @return permission service to check if current user has the necessary + * permissions. + */ + @Bean + @ConditionalOnMissingBean + public PermissionService permissionService() { + return new PermissionService(); + } + + /** + * Creates the auditore aware. + * + * @return the spring security auditore aware + */ + @Bean + @ConditionalOnMissingBean + public AuditorAware auditorAware() { + return new SpringSecurityAuditorAware(); + } + + /** + * Auto-configuration for the in-memory-user-management. + * + * + * + */ + @Configuration + @ConditionalOnMissingBean(value = { UserAuthenticationFilter.class }) + public static class InMemoryUserManagementConfiguration extends GlobalAuthenticationConfigurerAdapter { + + private static final Logger LOGGER = LoggerFactory.getLogger(InMemoryUserManagementConfiguration.class); + + @Autowired + private AuthenticationConfiguration configuration; + + /* + * (non-Javadoc) + * + * @see org.springframework.security.config.annotation.authentication. + * configurers. GlobalAuthenticationConfigurerAdapter + * #configure(org.springframework.security.config.annotation. + * authentication.builders.AuthenticationManagerBuilder) + */ + @Override + public void configure(final AuthenticationManagerBuilder auth) throws Exception { + final DaoAuthenticationProvider userDaoAuthenticationProvider = new TenantDaoAuthenticationProvider(); + userDaoAuthenticationProvider.setUserDetailsService(userDetailsService()); + auth.authenticationProvider(userDaoAuthenticationProvider); + } + + /** + * @return the user details service to load a user from memory user + * manager. + */ + @Bean + public UserDetailsService userDetailsService() { + final InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager( + new ArrayList<>()); + inMemoryUserDetailsManager.setAuthenticationManager(null); + inMemoryUserDetailsManager.createUser(new User("admin", "admin", getAllAuthorities())); + return inMemoryUserDetailsManager; + } + + /** + * @return the multi-tenancy indicator to disallow multi-tenancy + */ + @Bean + public MultitenancyIndicator multiTenancyIndicator() { + return new MultitenancyIndicator() { + @Override + public boolean isMultiTenancySupported() { + return false; + } + }; + } + + private Collection getAllAuthorities() { + final List allPermissions = new ArrayList<>(); + final Field[] declaredFields = SpPermission.class.getDeclaredFields(); + for (final Field field : declaredFields) { + if (Modifier.isPublic(field.getModifiers()) && Modifier.isStatic(field.getModifiers())) { + field.setAccessible(true); + try { + final String permissionName = (String) field.get(null); + allPermissions.add(new SimpleGrantedAuthority(permissionName)); + } catch (final IllegalAccessException e) { + LOGGER.error(e.getMessage(), e); + } + } + } + return allPermissions; + } + + private static class TenantDaoAuthenticationProvider extends DaoAuthenticationProvider { + /* + * (non-Javadoc) + * + * @see org.springframework.security.authentication.dao. + * AbstractUserDetailsAuthenticationProvider + * #createSuccessAuthentication(java.lang.Object, + * org.springframework.security.core.Authentication, + * org.springframework.security.core.userdetails.UserDetails) + */ + @Override + protected Authentication createSuccessAuthentication(final Object principal, + final Authentication authentication, final UserDetails user) { + final UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, + authentication.getCredentials(), user.getAuthorities()); + result.setDetails(new TenantAwareAuthenticationDetails("DEFAULT", false)); + return result; + } + } + + /** + * @return the {@link UserAuthenticationFilter} to include into the SP + * security configuration. + * @throws Exception + * lazy bean exception maybe if the authentication manager + * cannot be instantiated + */ + @Bean + public UserAuthenticationFilter userAuthenticationFilter() throws Exception { + return new UserAuthenticationFilterBasicAuth(configuration.getAuthenticationManager()); + } + + private static final class UserAuthenticationFilterBasicAuth extends BasicAuthenticationFilter + implements UserAuthenticationFilter { + + private UserAuthenticationFilterBasicAuth(final AuthenticationManager authenticationManager) { + super(authenticationManager); + } + + } + } + +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java new file mode 100755 index 000000000..1c8b32ffb --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java @@ -0,0 +1,563 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.security; + +import java.io.IOException; +import java.net.URI; +import java.util.Collections; + +import javax.annotation.PostConstruct; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.ExcludePathAwareShallowETagFilter; +import org.eclipse.hawkbit.cache.CacheConstants; +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.eclipse.hawkbit.im.authentication.TenantUserPasswordAuthenticationToken; +import org.eclipse.hawkbit.im.authentication.UserAuthenticationFilter; +import org.eclipse.hawkbit.repository.ControllerManagement; +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.rest.resource.RestConstants; +import org.eclipse.hawkbit.security.ControllerTenantAwareAuthenticationDetailsSource; +import org.eclipse.hawkbit.security.DosFilter; +import org.eclipse.hawkbit.security.HttpControllerPreAuthenticateSecurityTokenFilter; +import org.eclipse.hawkbit.security.HttpControllerPreAuthenticatedGatewaySecurityTokenFilter; +import org.eclipse.hawkbit.security.HttpControllerPreAuthenticatedSecurityHeaderFilter; +import org.eclipse.hawkbit.security.HttpDownloadAuthenticationFilter; +import org.eclipse.hawkbit.security.PreAuthTokenSourceTrustAuthenticationProvider; +import org.eclipse.hawkbit.security.SecurityProperties; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.boot.context.embedded.FilterRegistrationBean; +import org.springframework.cache.Cache; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.WebSecurityConfigurer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter; +import org.springframework.security.web.header.writers.frameoptions.StaticAllowFromStrategy; +import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter; +import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter.XFrameOptionsMode; +import org.vaadin.spring.security.VaadinSecurityContext; +import org.vaadin.spring.security.annotation.EnableVaadinSecurity; +import org.vaadin.spring.security.web.VaadinDefaultRedirectStrategy; +import org.vaadin.spring.security.web.VaadinRedirectStrategy; +import org.vaadin.spring.security.web.authentication.VaadinAuthenticationSuccessHandler; +import org.vaadin.spring.security.web.authentication.VaadinUrlAuthenticationSuccessHandler; + +/** + * All configurations related to SP authentication and authorization layer. + * + * + * + */ +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true, mode = AdviceMode.ASPECTJ, proxyTargetClass = true, securedEnabled = true) +@EnableWebMvcSecurity +@Order(value = Ordered.HIGHEST_PRECEDENCE) +public class SecurityManagedConfiguration implements EnvironmentAware { + private static final Logger LOG = LoggerFactory.getLogger(SecurityManagedConfiguration.class); + + private static final String SP_SERVER_CONFIG_PREFIX = "hawkbit.server."; + private RelaxedPropertyResolver environment; + + @Override + public void setEnvironment(final Environment environment) { + this.environment = new RelaxedPropertyResolver(environment, SP_SERVER_CONFIG_PREFIX); + + } + + /** + * {@link WebSecurityConfigurer} for the internal SP controller API. + * + * + * + */ + @Configuration + @Order(300) + static class ControllerSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter { + + @Autowired + private ControllerManagement controllerManagement; + @Autowired + private SystemManagement systemManagement; + @Autowired + private TenantAware tenantAware; + @Autowired + private SecurityProperties securityConfiguration; + @Autowired + private org.springframework.boot.autoconfigure.security.SecurityProperties springSecurityProperties; + + @Override + protected void configure(final HttpSecurity http) throws Exception { + final ControllerTenantAwareAuthenticationDetailsSource authenticationDetailsSource = new ControllerTenantAwareAuthenticationDetailsSource(); + + final HttpControllerPreAuthenticatedSecurityHeaderFilter securityHeaderFilter = new HttpControllerPreAuthenticatedSecurityHeaderFilter( + securityConfiguration.getRpCnHeader(), securityConfiguration.getRpSslIssuerHashHeader(), + systemManagement, tenantAware); + securityHeaderFilter.setAuthenticationManager(authenticationManager()); + securityHeaderFilter.setCheckForPrincipalChanges(true); + securityHeaderFilter.setAuthenticationDetailsSource(authenticationDetailsSource); + + final HttpControllerPreAuthenticateSecurityTokenFilter securityTokenFilter = new HttpControllerPreAuthenticateSecurityTokenFilter( + systemManagement, tenantAware, controllerManagement); + securityTokenFilter.setAuthenticationManager(authenticationManager()); + securityTokenFilter.setCheckForPrincipalChanges(true); + securityTokenFilter.setAuthenticationDetailsSource(authenticationDetailsSource); + + final HttpControllerPreAuthenticatedGatewaySecurityTokenFilter gatewaySecurityTokenFilter = new HttpControllerPreAuthenticatedGatewaySecurityTokenFilter( + systemManagement, tenantAware); + gatewaySecurityTokenFilter.setAuthenticationManager(authenticationManager()); + gatewaySecurityTokenFilter.setCheckForPrincipalChanges(true); + gatewaySecurityTokenFilter.setAuthenticationDetailsSource(authenticationDetailsSource); + + HttpSecurity httpSec = http.csrf().disable().headers() + .addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.DENY)).contentTypeOptions() + .xssProtection().httpStrictTransportSecurity().and(); + + if (springSecurityProperties.isRequireSsl()) { + httpSec = httpSec.requiresChannel().anyRequest().requiresSecure().and(); + } + + if (securityConfiguration.getAnonymousEnabled()) { + LOG.info( + "******************\n** Anonymous controller security enabled, should only use for developing purposes **\n******************"); + final AnonymousAuthenticationFilter anoymousFilter = new AnonymousAuthenticationFilter( + "controllerAnonymousFilter", "anonymous", Collections.singletonList( + new SimpleGrantedAuthority(SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS))); + anoymousFilter.setAuthenticationDetailsSource(authenticationDetailsSource); + httpSec.requestMatchers().antMatchers("/*/controller/v1/**", "/*/controller/artifacts/v1/**").and() + .securityContext().disable().anonymous().authenticationFilter(anoymousFilter); + } else { + httpSec.addFilter(securityHeaderFilter).addFilter(securityTokenFilter) + .addFilter(gatewaySecurityTokenFilter).antMatcher("/*/controller/**").anonymous().disable() + .authorizeRequests().anyRequest().authenticated().and().exceptionHandling() + .authenticationEntryPoint(new AuthenticationEntryPoint() { + @Override + public void commence(final HttpServletRequest request, final HttpServletResponse response, + final AuthenticationException authException) throws IOException, ServletException { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + } + }).and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + } + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.security.config.annotation.web.configuration. + * WebSecurityConfigurerAdapter + * #configure(org.springframework.security.config.annotation. + * authentication.builders. AuthenticationManagerBuilder) + */ + @Override + protected void configure(final AuthenticationManagerBuilder auth) throws Exception { + auth.authenticationProvider( + new PreAuthTokenSourceTrustAuthenticationProvider(securityConfiguration.getRpTrustedIPs())); + } + } + + /** + * Filter to protect the SP server against denial of service attacks. + * + * @return he spring filter registration bean for registering an denial of + * service protection filter in the filter chain + */ + @Bean + @Order(50) + public FilterRegistrationBean dosFilter() { + final FilterRegistrationBean filterRegBean = new FilterRegistrationBean(); + + filterRegBean + .setFilter( + new DosFilter(environment.getProperty("security.dos.filter.maxRead", Integer.class, 100), + environment.getProperty("security.dos.filter.maxWrite", Integer.class, 10), + environment.getProperty("security.dos.filter.whitelist"), environment + .getProperty("security.clients.blacklist"), + environment.getProperty("security.rp.remote_ip_header", String.class, "X-Forwarded-For"))); + filterRegBean.addUrlPatterns("/{tenant}/controller/v1/*", "/rest/*"); + return filterRegBean; + } + + /** + * Filter registration bean for spring etag filter. + * + * @return the spring filter registration bean for registering an etag + * filter in the filter chain + */ + @Bean + @Order(100) + public FilterRegistrationBean eTagFilter() { + final FilterRegistrationBean filterRegBean = new FilterRegistrationBean(); + // eclude the URLs for downloading artifacts, so no eTag is generated in + // the + // ShallowEtagHeaderFilter, just using the SH1 hash of the artifact + // itself as 'ETag', because + // otherwise the file will be copied in memory! + filterRegBean.setFilter(new ExcludePathAwareShallowETagFilter( + "/rest/v1/softwaremodules/{smId}/artifacts/{artId}/download", "/{tenant}/controller/artifacts/**", + "/{targetid}/softwaremodules/{softwareModuleId}/artifacts/**")); + return filterRegBean; + } + + /** + * Security configuration for the REST management API of the health url. + * + * + */ + @Configuration + @Order(310) + public static class HealthSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(final HttpSecurity http) throws Exception { + http.regexMatcher("/system/health").csrf().disable().httpBasic().and().sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + } + } + + /** + * Security configuration for the REST management API. + * + * + */ + @Configuration + @Order(350) + public static class RestSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter { + @Autowired + private UserAuthenticationFilter userAuthenticationFilter; + @Autowired + private SystemManagement systemManagement; + @Autowired + private TenantAware tenantAware; + @Autowired + private org.springframework.boot.autoconfigure.security.SecurityProperties springSecurityProperties; + + @Override + protected void configure(final HttpSecurity http) throws Exception { + + HttpSecurity httpSec = http.regexMatcher("\\/rest.*|\\/system.*").csrf().disable(); + if (springSecurityProperties.isRequireSsl()) { + httpSec = httpSec.requiresChannel().anyRequest().requiresSecure().and(); + } + httpSec.addFilterBefore(new Filter() { + @Override + public void init(final FilterConfig filterConfig) throws ServletException { + userAuthenticationFilter.init(filterConfig); + } + + @Override + public void doFilter(final ServletRequest request, final ServletResponse response, + final FilterChain chain) throws IOException, ServletException { + userAuthenticationFilter.doFilter(request, response, chain); + } + + @Override + public void destroy() { + userAuthenticationFilter.destroy(); + } + }, RequestHeaderAuthenticationFilter.class) + .addFilterAfter( + new AuthenticationSuccessTenantMetadataCreationFilter(tenantAware, systemManagement), + RequestHeaderAuthenticationFilter.class) + .authorizeRequests().anyRequest().authenticated() + .antMatchers(RestConstants.BASE_SYSTEM_MAPPING + "/admin/**") + .hasAnyAuthority(SpPermission.SYSTEM_ADMIN).antMatchers(RestConstants.BASE_SYSTEM_MAPPING + "/**") + .hasAnyAuthority(SpPermission.SYSTEM_DIAG); + } + } + + /** + * {@link WebSecurityConfigurer} for external (management) access. + * + * + */ + @Configuration + @Order(400) + @EnableVaadinSecurity + public static class UISecurityConfigurationAdapter extends WebSecurityConfigurerAdapter + implements EnvironmentAware { + + private static final String XFRAME_OPTION_DENY = "DENY"; + private static final String XFRAME_OPTION_SAMEORIGIN = "SAMEORIGIN"; + private static final String XFAME_OPTION_ALLOW_FROM = "ALLOW-FROM"; + @Autowired + private VaadinSecurityContext vaadinSecurityContext; + @Autowired + private org.springframework.boot.autoconfigure.security.SecurityProperties springSecurityProperties; + + private RelaxedPropertyResolver environment; + + @Override + public void setEnvironment(final Environment environment) { + this.environment = new RelaxedPropertyResolver(environment, SP_SERVER_CONFIG_PREFIX); + } + + /** + * post construct for setting the authentication success handler for the + * vaadin security context. + */ + @PostConstruct + public void afterPropertiesSet() { + this.vaadinSecurityContext.addAuthenticationSuccessHandler(redirectSaveHandler()); + } + + @Bean(name = "authenticationManager") + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + /** + * @return The VaadinRedirectStategy + */ + @Bean + public VaadinRedirectStrategy vaadinRedirectStrategy() { + return new VaadinDefaultRedirectStrategy(); + } + + /** + * @return the vaadin success authentication handler + */ + @Bean + public VaadinAuthenticationSuccessHandler redirectSaveHandler() { + final VaadinUrlAuthenticationSuccessHandler handler = new TenantMetadataSavedRequestAwareVaadinAuthenticationSuccessHandler(); + handler.setRedirectStrategy(vaadinRedirectStrategy()); + handler.setDefaultTargetUrl("/UI/"); + handler.setTargetUrlParameter("r"); + return handler; + } + + @Override + protected void configure(final HttpSecurity http) throws Exception { + + // configuration xframe-option + final String confXframeOption = environment.getProperty("security.xframe.option", XFRAME_OPTION_DENY); + final String confAllowFromUri = environment.getProperty("security.xframe.option.allowfrom"); + if (confXframeOption.equals(XFAME_OPTION_ALLOW_FROM) && confAllowFromUri == null) { + // if allow-from option is specified but no allowFromUri throw + // exception + throw new IllegalStateException("hawkbit.server.security.xframe.option has been specified as ALLOW-FROM" + + " but no hawkbit.server.security.xframe.option.allowfrom has been set, " + + "please ensure to set allow from URIs"); + } + + // workaround regex: we need to exclude the URL /UI/HEARTBEAT here + // because we bound the + // vaadin application to /UI and not to root, described in + // vaadin-forum: + // https://vaadin.com/forum#!/thread/3200565. + HttpSecurity httpSec = http.regexMatcher("(?!.*HEARTBEAT)^.*\\/UI.*$") + // disable as CSRF is handled by Vaadin + .csrf().disable(); + + if (springSecurityProperties.isRequireSsl()) { + httpSec = httpSec.requiresChannel().anyRequest().requiresSecure().and(); + } else { + LOG.info( + "\"******************\\n** Requires HTTPS Security has been disabled for UI, should only use for developing purposes **\\n******************\""); + } + + // for UI integrator we allow frame integration on same origin + httpSec.headers() + .addHeaderWriter(confXframeOption.equals(XFAME_OPTION_ALLOW_FROM) + ? new XFrameOptionsHeaderWriter(new StaticAllowFromStrategy(new URI(confAllowFromUri))) + : new XFrameOptionsHeaderWriter(xframeOptionFromStr(confXframeOption))) + .contentTypeOptions().xssProtection().httpStrictTransportSecurity().and() + // UI + .authorizeRequests().antMatchers("/UI/login/**").permitAll().antMatchers("/UI/UIDL/**").permitAll() + .anyRequest().authenticated().and() + // UI login / logout + .exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/UI/login/#/")) + .and().logout().logoutUrl("/UI/logout").logoutSuccessUrl("/UI/login/#/"); + } + + /** + * Converts a given string into the {@link XFrameOptionsMode} enum. Only + * {@link XFrameOptionsMode#DENY} and + * {@link XFrameOptionsMode#SAMEORIGIN} any other string will be + * converted to the default {@link XFrameOptionsMode#SAMEORIGIN}. + * + * @param xframeOption + * the string of the xframe option + * @return an {@link XFrameOptionsMode} by the given string, in case + * string does not match an option then + * {@link XFrameOptionsMode#SAMEORIGIN} is returned + */ + private XFrameOptionsMode xframeOptionFromStr(@NotNull final String xframeOption) { + switch (xframeOption) { + case XFRAME_OPTION_DENY: + return XFrameOptionsMode.DENY; + case XFRAME_OPTION_SAMEORIGIN: + // fall through to default because the same + default: + return XFrameOptionsMode.SAMEORIGIN; + } + } + + @Override + public void configure(final WebSecurity webSecurity) throws Exception { + webSecurity.ignoring().antMatchers("/documentation/**", "/VAADIN/**", "/*.*", "/v2/api-docs/**", + "/docs/**"); + } + } + + /** + * A Websecruity config to handle and filter the download ids. + * + * + * + */ + @Configuration + @EnableWebSecurity + @Order(200) + public static class IdRestSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter { + + @Autowired + private SecurityProperties securityConfiguration; + + @Autowired + @Qualifier(CacheConstants.DOWNLOAD_ID_CACHE) + private Cache downloadIdCache; + + @Override + protected void configure(final HttpSecurity http) throws Exception { + final HttpDownloadAuthenticationFilter downloadIdAuthenticationFilter = new HttpDownloadAuthenticationFilter( + downloadIdCache); + downloadIdAuthenticationFilter.setAuthenticationManager(authenticationManager()); + + http.csrf().disable(); + http.anonymous().disable(); + + http.regexMatcher(HttpDownloadAuthenticationFilter.REQUEST_ID_REGEX_PATTERN) + .addFilterBefore(downloadIdAuthenticationFilter, FilterSecurityInterceptor.class); + http.authorizeRequests().anyRequest().authenticated(); + } + + @Override + protected void configure(final AuthenticationManagerBuilder auth) throws Exception { + auth.authenticationProvider( + new PreAuthTokenSourceTrustAuthenticationProvider(securityConfiguration.getRpTrustedIPs())); + } + + } + +} + +/** + * After a successful login on the UI we need to ensure to create the tenant + * meta data within SP. + * + * + */ +class TenantMetadataSavedRequestAwareVaadinAuthenticationSuccessHandler extends VaadinUrlAuthenticationSuccessHandler { + + @Autowired + private SystemManagement systemManagement; + + /* + * (non-Javadoc) + * + * @see org.vaadin.spring.security.web.authentication. + * SavedRequestAwareVaadinAuthenticationSuccessHandler + * #onAuthenticationSuccess(org.springframework.security.core. + * Authentication) + */ + @Override + public void onAuthenticationSuccess(final Authentication authentication) throws Exception { + if (authentication.getClass().equals(TenantUserPasswordAuthenticationToken.class)) { + systemManagement + .getTenantMetadata(((TenantUserPasswordAuthenticationToken) authentication).getTenant().toString()); + } else if (authentication.getClass().equals(UsernamePasswordAuthenticationToken.class)) { + // TODO: MECS-1078 vaadin4spring-ext-security does not give us the + // fullyAuthenticatedToken + // in the GenericVaadinSecurity class. Only the token which has been + // created in the + // LoginView. This needs to be changed with the update of + // vaadin4spring 0.0.7 because it + // has been fixed. + systemManagement.getTenantMetadata("DEFAULT"); + } + super.onAuthenticationSuccess(authentication); + } +} + +/** + * Sevletfilter to create metadata after successful authentication over RESTful. + * + * + */ +class AuthenticationSuccessTenantMetadataCreationFilter implements Filter { + + private final TenantAware tenantAware; + private final SystemManagement systemManagement; + + AuthenticationSuccessTenantMetadataCreationFilter(final TenantAware tenantAware, + final SystemManagement systemManagement) { + this.tenantAware = tenantAware; + this.systemManagement = systemManagement; + } + + @Override + public void init(final FilterConfig filterConfig) throws ServletException { + // not needed + } + + @Override + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) + throws IOException, ServletException { + final String currentTenant = tenantAware.getCurrentTenant(); + if (currentTenant != null) { + // lazy initialize tenant meta data after successful authentication + systemManagement.getTenantMetadata(currentTenant); + } + chain.doFilter(request, response); + } + + @Override + public void destroy() { + // not needed + } +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/swagger/SwaggerApiDocAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/swagger/SwaggerApiDocAutoConfiguration.java new file mode 100755 index 000000000..ae405242c --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/swagger/SwaggerApiDocAutoConfiguration.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.swagger; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.hawkbit.rest.resource.RestConstants; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.bind.annotation.RequestMethod; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; + +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.schema.ModelRef; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.ResponseMessage; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +/** + * Swagger configuration for RESTful SP server APIs. + * + * + * + * + */ + +@Configuration +@EnableSwagger2 +public class SwaggerApiDocAutoConfiguration { + + /** + * Create the Springfox Docket, which generates the Information for the + * REST-Swagger-UI. Rest paths are: /rest/v1/. and ./controller/v1/. + * + * @see springfox.documentation.spring.web.plugins.Docket + * + * @return the v1 docket + */ + @Bean + public Docket customImplementation() { + return createDocket(); + } + + private Predicate selectApiPaths() { + return Predicates.or(PathSelectors.regex("/rest/v1/.*"), PathSelectors.regex("/.*/controller/v1/.*")); + } + + private Docket createDocket() { + final List authorizationMessages = globalAuhtorizationMessages(); + return new Docket(DocumentationType.SWAGGER_2).select().paths(selectApiPaths()).build() + .useDefaultResponseMessages(true).globalResponseMessage(RequestMethod.GET, authorizationMessages) + .globalResponseMessage(RequestMethod.POST, authorizationMessages) + .globalResponseMessage(RequestMethod.PUT, authorizationMessages) + .globalResponseMessage(RequestMethod.DELETE, authorizationMessages).apiInfo(apiInfo()); + } + + private List globalAuhtorizationMessages() { + final List messageList = new ArrayList<>(); + messageList.add(new ResponseMessage(200, "Request sucessfull", new ModelRef("com."))); + messageList.add(new ResponseMessage(400, "Bad Request - e.g. invalid parameters", null)); + messageList.add(new ResponseMessage(401, "Unauthorized - The request requires user authentication.", + + null)); + messageList.add( + new ResponseMessage(403, "Forbidden - Insufficient permissions or data volume restriction applies.", + new ModelRef("ExceptionInfo"))); + messageList.add(new ResponseMessage(405, "Method Not Allowed", null)); + messageList.add(new ResponseMessage(406, + "Not Acceptable - In case accept header is specified and not application/json", null)); + messageList.add(new ResponseMessage(429, + "Too many requests. The server will refuse further attemps and the client has to wait another second.", + null)); + return messageList; + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder().title("Software Provisioning API Descriptions").version(RestConstants.API_VERSION) + .build(); + } + +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/ui/UIAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/ui/UIAutoConfiguration.java new file mode 100755 index 000000000..ff73eff7b --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/ui/UIAutoConfiguration.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.ui; + +import org.eclipse.hawkbit.DistributedResourceBundleMessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.vaadin.spring.annotation.EnableVaadinExtensions; +import org.vaadin.spring.events.annotation.EnableEventBus; +import org.vaadin.spring.security.annotation.EnableVaadinSecurity; + +/** + * The hawkbit-ui autoconfiguration. + * + * + * + */ +@Configuration +@EnableVaadinSecurity +@EnableVaadinExtensions +@EnableEventBus +public class UIAutoConfiguration { + + /** + * A message source bean to add distributed message sources. + * + * @return the message bean. + */ + @Bean(name = "messageSource") + public DistributedResourceBundleMessageSource messageSource() { + return new DistributedResourceBundleMessageSource(); + } + +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/url/PropertyHostnameResolverAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/url/PropertyHostnameResolverAutoConfiguration.java new file mode 100755 index 000000000..eb925e281 --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/url/PropertyHostnameResolverAutoConfiguration.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.url; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.eclipse.hawkbit.api.HostnameResolver; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.google.common.base.Throwables; + +/** + * Autoconfiguration of the {@link HostnameResolver} based on a property. + * + * + * + */ +@Configuration +@EnableConfigurationProperties(ServerProperties.class) +public class PropertyHostnameResolverAutoConfiguration { + + @Autowired + private ServerProperties serverProperties; + + /** + * @return the default autoconfigure hostname resolver implementation which + * is property based specified by the property {@link #url} + */ + @Bean + @ConditionalOnMissingBean(value = HostnameResolver.class) + public HostnameResolver hostnameResolver() { + return new HostnameResolver() { + @Override + public URL resolveHostname() { + try { + return new URL(serverProperties.getUrl()); + } catch (final MalformedURLException e) { + throw Throwables.propagate(e); + } + } + }; + } + +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/url/ServerProperties.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/url/ServerProperties.java new file mode 100755 index 000000000..24c9dfb0e --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/url/ServerProperties.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.url; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Properties for the server e.g. the server's URL which must be configured. + * + * + */ +@ConfigurationProperties("hawkbit.server") +public class ServerProperties { + + private String url = "http://localhost:8080"; + + public String getUrl() { + return url; + } + + public void setUrl(final String url) { + this.url = url; + } +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/web/ResourceControllerAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/web/ResourceControllerAutoConfiguration.java new file mode 100755 index 000000000..1d75510e4 --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/web/ResourceControllerAutoConfiguration.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.web; + +import org.eclipse.hawkbit.controller.EnableDirectDeviceApi; +import org.eclipse.hawkbit.rest.resource.EnableRestResources; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * Auto-Configuration for enabling the REST-Resources. + * + * + * + */ +@Configuration +@ConditionalOnClass({ EnableDirectDeviceApi.class, EnableRestResources.class }) +@Import({ EnableDirectDeviceApi.class, EnableRestResources.class }) +public class ResourceControllerAutoConfiguration { + +} diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/web/WebMvcAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/web/WebMvcAutoConfiguration.java new file mode 100755 index 000000000..03140f302 --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/web/WebMvcAutoConfiguration.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.web; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +/** + * A configuration bean which disables the {@code useSuffixPatternMatch} feature + * from Spring because it will truncate the dot in a REST URL which leads to + * problem in case a controllerId contains dots and is a path parameter or + * filename ending. + * + * + */ +@Configuration +public class WebMvcAutoConfiguration extends WebMvcConfigurerAdapter { + + /* + * (non-Javadoc) + * + * @see + * org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter + * #configurePathMatch + * (org.springframework.web.servlet.config.annotation.PathMatchConfigurer) + */ + @Override + public void configurePathMatch(final PathMatchConfigurer configurer) { + configurer.setUseSuffixPatternMatch(false); + configurer.setUseRegisteredSuffixPatternMatch(false); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.web.servlet.config.annotation. + * WebMvcConfigurerAdapter# configureContentNegotiation + * (org.springframework.web.servlet.config.annotation. + * ContentNegotiationConfigurer) + */ + @Override + public void configureContentNegotiation(final ContentNegotiationConfigurer configurer) { + configurer.favorPathExtension(false); + } +} diff --git a/hawkbit-autoconfigure/src/main/resources/META-INF/spring.factories b/hawkbit-autoconfigure/src/main/resources/META-INF/spring.factories new file mode 100755 index 000000000..c0563d7c9 --- /dev/null +++ b/hawkbit-autoconfigure/src/main/resources/META-INF/spring.factories @@ -0,0 +1,16 @@ +# Auto Configure +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.eclipse.hawkbit.autoconfigure.conf.DefaultPropertiesAutoConfiguration,\ +org.eclipse.hawkbit.autoconfigure.ui.UIAutoConfiguration,\ +org.eclipse.hawkbit.autoconfigure.security.SecurityAutoConfiguration,\ +org.eclipse.hawkbit.autoconfigure.swagger.SwaggerApiDocAutoConfiguration,\ +org.eclipse.hawkbit.autoconfigure.url.PropertyHostnameResolverAutoConfiguration,\ +org.eclipse.hawkbit.autoconfigure.web.WebMvcAutoConfiguration,\ +org.eclipse.hawkbit.autoconfigure.cache.CacheAutoConfiguration,\ +org.eclipse.hawkbit.autoconfigure.cache.DownloadIdCacheAutoConfiguration,\ +org.eclipse.hawkbit.autoconfigure.eventbus.EventBusAutoConfiguration,\ +org.eclipse.hawkbit.autoconfigure.scheduling.AsyncConfigurerAutoConfiguration,\ +org.eclipse.hawkbit.autoconfigure.cache.RedisAutoConfiguration,\ +org.eclipse.hawkbit.autoconfigure.scheduling.ExecutorAutoConfiguration,\ +org.eclipse.hawkbit.autoconfigure.conf.ControllerPollAutoConfiguration,\ +org.eclipse.hawkbit.autoconfigure.amqp.AmqpAutoConfiguration diff --git a/hawkbit-autoconfigure/src/main/resources/hawkbitdefaults.properties b/hawkbit-autoconfigure/src/main/resources/hawkbitdefaults.properties new file mode 100755 index 000000000..69c9a47d2 --- /dev/null +++ b/hawkbit-autoconfigure/src/main/resources/hawkbitdefaults.properties @@ -0,0 +1,57 @@ +# +# Copyright (c) 2015 Bosch Software Innovations GmbH and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# + +# Tomcat / Server +server.tomcat.compression=on +spring.http.gzip.mime-types=text/html,text/xml,text/plain,application/json,application/javascript,text/css,application/x-javascript,text/javascript,application/vnd.ms-fontobject,application/x-font-opentype,application/x-font-truetype,application/x-font-ttf,application/xml,font/eot,font/opentype,font/otf,image/svg+xml,image/vnd.microsoft.icon +server.tomcat.compressable-mime-types=${spring.http.gzip.mime-types} +spring.http.gzip.min-gzip-size=256 + +# JPA / Datasource +spring.jpa.eclipselink.eclipselink.weaving=false +spring.jpa.database=H2 +spring.jpa.show-sql=false +spring.datasource.driverClassName=org.h2.Driver + +# MongoDB for artifact-repository +spring.data.mongodb.uri=mongodb://localhost/artifactrepo +spring.data.mongo.repositories.enabled=true + +# Flyway DDL +flyway.enabled=true +flyway.initOnMigrate=true +flyway.sqlMigrationSuffix=${spring.jpa.database}.sql + +# Vaadin Servlet +vaadin.static.servlet.params.resourceCacheTime=${spring.resources.cache-period} +vaadin.static.servlet.params.productionMode=true +vaadin.servlet.params.productionMode=true +vaadin.servlet.params.resourceCacheTime=${spring.resources.cache-period} +vaadin.servlet.urlMapping=/UI/* +vaadin.servlet.params.heartbeatInterval=60 +vaadin.servlet.params.closeIdleSessions=false + +# Spring MVC +spring.mvc.favicon.enabled=false + + +# Defines the thread pool executor +hawkbit.threadpool.corethreads=5 +hawkbit.threadpool.maxthreads=20 +hawkbit.threadpool.idletimeout=10000 +hawkbit.threadpool.queuesize=250 + +# Defines the polling time for the controllers in HH:MM:SS notation +hawkbit.controller.pollingTime=00:05:00 +hawkbit.controller.pollingOverdueTime=00:05:00 + +## Configuration for RabbitMQ integration +hawkbit.dmf.rabbitmq.deadLetterQueue=dmf_connector_deadletter +hawkbit.dmf.rabbitmq.deadLetterExchange=dmf.connector.deadletter +hawkbit.dmf.rabbitmq.receiverQueue=dmf_receiver diff --git a/hawkbit-cache-redis/pom.xml b/hawkbit-cache-redis/pom.xml new file mode 100755 index 000000000..3ed2d62f3 --- /dev/null +++ b/hawkbit-cache-redis/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + org.eclipse.hawkbit + hawkbit-parent + 0.1.0-SNAPSHOT + + hawkbit-cache-redis + hawkBit :: Redis Cache Manager + + + + org.eclipse.hawkbit + hawkbit-core + ${project.version} + + + org.springframework.data + spring-data-redis + + + org.springframework.boot + spring-boot + + + redis.clients + jedis + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.easytesting + fest-assert-core + test + + + org.easytesting + fest-assert + test + + + org.springframework.hateoas + spring-hateoas + test + + + ru.yandex.qatools.allure + allure-junit-adaptor + test + + + + \ No newline at end of file diff --git a/hawkbit-cache-redis/src/main/java/org/eclipse/hawkbit/cache/RedisConfiguration.java b/hawkbit-cache-redis/src/main/java/org/eclipse/hawkbit/cache/RedisConfiguration.java new file mode 100755 index 000000000..2d1b99c98 --- /dev/null +++ b/hawkbit-cache-redis/src/main/java/org/eclipse/hawkbit/cache/RedisConfiguration.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.cache; + +import org.eclipse.hawkbit.cache.eventbus.EventDistributor; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; +import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; + +/** + * The spring Redis configuration which is enabled by using the profile + * {@code redis} to use a Redis server as cache. + * + * + * + */ +@Configuration +@EnableConfigurationProperties(RedisProperties.class) +public class RedisConfiguration { + + @Autowired + private RedisProperties redisProperties; + + @Autowired + private TenantAware tenantAware; + + /** + * @return the {@link EventDistributor} to distribute and consume the events + * from Redis + */ + @Bean + public EventDistributor eventDistributor() { + return new EventDistributor(); + } + + /** + * @return the spring redis cache manager. + */ + @Bean + public CacheManager cacheManager() { + return new TenantAwareCacheManager(new RedisCacheManager(redisTemplate()), tenantAware); + } + + /** + * @return the redis connection factory to create a redis connection based + * on the {@link RedisProperties} + */ + @Bean + public JedisConnectionFactory jedisConnectionFactory() { + final JedisConnectionFactory factory = new JedisConnectionFactory(); + factory.setHostName(redisProperties.getHost()); + factory.setPort(redisProperties.getPort()); + factory.setUsePool(true); + return factory; + } + + /** + * @return the spring {@link RedisTemplate} configured with the necessary + * object serializers + */ + @Bean + public RedisTemplate redisTemplate() { + final RedisTemplate redisTemplate = new RedisTemplate(); + redisTemplate.setConnectionFactory(jedisConnectionFactory()); + redisTemplate.setKeySerializer(new JdkSerializationRedisSerializer()); + redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer()); + redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer()); + return redisTemplate; + } + + /** + * @return the spring-redis message listener adapter to consume messages + * from the Redis server + */ + @Bean + public MessageListenerAdapter messageListenerAdapter() { + final MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(eventDistributor()); + messageListenerAdapter.setSerializer(new JdkSerializationRedisSerializer()); + return messageListenerAdapter; + } + + /** + * @return the spring-redis message listener container to register the + * message listener adapter + */ + @Bean + public RedisMessageListenerContainer redisContainer() { + final RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(jedisConnectionFactory()); + container.addMessageListener(messageListenerAdapter(), eventDistributor().getTopics()); + return container; + } + +} diff --git a/hawkbit-cache-redis/src/main/java/org/eclipse/hawkbit/cache/RedisProperties.java b/hawkbit-cache-redis/src/main/java/org/eclipse/hawkbit/cache/RedisProperties.java new file mode 100755 index 000000000..c228cde4c --- /dev/null +++ b/hawkbit-cache-redis/src/main/java/org/eclipse/hawkbit/cache/RedisProperties.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.cache; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Bean which holds the necessary properties for configuring the Redis + * connection. + * + * + * + * + */ +@ConfigurationProperties("hawkbit.server.redis") +public class RedisProperties { + + private String host; + private int port; + + /** + * @return the host + */ + public String getHost() { + return host; + } + + /** + * @param host + * the host to set + */ + public void setHost(final String host) { + this.host = host; + } + + /** + * @return the port + */ + public int getPort() { + return port; + } + + /** + * @param port + * the port to set + */ + public void setPort(final int port) { + this.port = port; + } +} diff --git a/hawkbit-cache-redis/src/main/java/org/eclipse/hawkbit/cache/annotation/EnableRedis.java b/hawkbit-cache-redis/src/main/java/org/eclipse/hawkbit/cache/annotation/EnableRedis.java new file mode 100755 index 000000000..eb88a6d9d --- /dev/null +++ b/hawkbit-cache-redis/src/main/java/org/eclipse/hawkbit/cache/annotation/EnableRedis.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.cache.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.hawkbit.cache.RedisConfiguration; +import org.springframework.context.annotation.Import; + +/** + * Annotation to enable redis caching. + * + * + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(RedisConfiguration.class) +public @interface EnableRedis { + +} diff --git a/hawkbit-cache-redis/src/main/java/org/eclipse/hawkbit/cache/eventbus/EventDistributor.java b/hawkbit-cache-redis/src/main/java/org/eclipse/hawkbit/cache/eventbus/EventDistributor.java new file mode 100755 index 000000000..e52128cbc --- /dev/null +++ b/hawkbit-cache-redis/src/main/java/org/eclipse/hawkbit/cache/eventbus/EventDistributor.java @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.cache.eventbus; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +import org.eclipse.hawkbit.eventbus.EventSubscriber; +import org.eclipse.hawkbit.eventbus.event.DistributedEvent; +import org.eclipse.hawkbit.eventbus.event.Event; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.Topic; +import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; + +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; + +/** + * + * + */ +@EventSubscriber +public class EventDistributor { + + private static final Logger LOGGER = LoggerFactory.getLogger(EventDistributor.class); + /** + * the node id to filter self published events in the redis message + * subscriber. + */ + private static final String NODE_ID = UUID.randomUUID().toString(); + + private static final String DISTRIBUTION_CHANNEL_TOPIC = "com/bosch/sp/distEvent"; + private static final String SEND_DISTRIBUTION_CHANNEL = DISTRIBUTION_CHANNEL_TOPIC + "/" + NODE_ID; + private static final String SUB_DISTRIBUTION_CHANNEL = DISTRIBUTION_CHANNEL_TOPIC + "*"; + + @Autowired(required = false) + private RedisTemplate redisTemplate; + + @Autowired + private EventBus eventBus; + + /** + * consumes all {@link DistributedEvent}s posted on the {@link EventBus} and + * distribute them to the Redis server. + * + * @param event + * the distributed event posted on the {@link EventBus} + */ + @Subscribe + public void distribute(final DistributedEvent event) { + if (redisTemplate != null) { + if (!NODE_ID.equals(event.getNodeId())) { + logDistributingEvent(event, SEND_DISTRIBUTION_CHANNEL); + event.setOriginNodeId(NODE_ID); + redisTemplate.convertAndSend(SEND_DISTRIBUTION_CHANNEL, event); + } + } else { + logNotDistributingEvent(event, SEND_DISTRIBUTION_CHANNEL); + } + } + + /** + * message listener callback method for the {@link MessageListenerAdapter} + * which calls the method in case a message is received from the Redis + * server of the type {@link DistributedEventWrapper}. + * + * @param event + * the {@link DistributedEventWrapper} event which was received + * by the Redis client + * @param channel + * the on which the event was received + */ + public void handleMessage(final DistributedEvent event, final String channel) { + LOGGER.trace("retrieving event from redis {} on channel {}, posting to the local event bus", event, channel); + if (!NODE_ID.equals(event.getOriginNodeId())) { + event.setNodeId(NODE_ID); + eventBus.post(event); + } + } + + /** + * @return a collection of all topics which this Redis message listener + * wants to subscribe + */ + public Collection getTopics() { + final List topics = new ArrayList(); + topics.add(new PatternTopic(SUB_DISTRIBUTION_CHANNEL)); + return topics; + } + + private void logDistributingEvent(final Event event, final String channel) { + LOGGER.trace("distributing event {} from node {} to topic {}", event, NODE_ID, channel); + } + + private void logNotDistributingEvent(final Event event, final String channel) { + LOGGER.debug("no redis template configured, event {} will not be distributed to channel {} from node {}", event, + channel, NODE_ID); + } + + /** + * testing purposes. + * + * @param redisTemplate + * the redisTemplate to set + */ + void setRedisTemplate(final RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + /** + * testing purposes. + * + * @param eventBus + * the eventBus to set + */ + void setEventBus(final EventBus eventBus) { + this.eventBus = eventBus; + } + + /** + * @return the nodeId + */ + static String getNodeId() { + return NODE_ID; + } +} diff --git a/hawkbit-cache-redis/src/test/java/org/eclipse/hawkbit/cache/eventbus/EventDistributorTest.java b/hawkbit-cache-redis/src/test/java/org/eclipse/hawkbit/cache/eventbus/EventDistributorTest.java new file mode 100755 index 000000000..e44889f60 --- /dev/null +++ b/hawkbit-cache-redis/src/test/java/org/eclipse/hawkbit/cache/eventbus/EventDistributorTest.java @@ -0,0 +1,222 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.cache.eventbus; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.Collection; + +import org.eclipse.hawkbit.eventbus.event.DownloadProgressEvent; +import org.eclipse.hawkbit.eventbus.event.EntityEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.listener.Topic; +import org.springframework.hateoas.Identifiable; + +import com.google.common.eventbus.EventBus; + +@RunWith(MockitoJUnitRunner.class) +public class EventDistributorTest { + + @Mock + private RedisTemplate redisTemplateMock; + + @Mock + private EventBus eventBusMock; + + private EventDistributor underTest; + + @Before + public void before() { + underTest = new EventDistributor(); + underTest.setEventBus(eventBusMock); + underTest.setRedisTemplate(redisTemplateMock); + } + + @Test + public void distributeDistributedEventSendsToRedis() { + + final DownloadProgressEvent event = new DownloadProgressEvent("tenant", 123L, 10); + underTest.distribute(event); + + // origin node ID should be set by distributing the event + assertThat(event.getOriginNodeId()).isNotNull(); + verify(redisTemplateMock).convertAndSend(anyString(), eq(event)); + } + + @Test + public void dontDistributeDistributedEventIfSameNode() { + final String knownNodeId = EventDistributor.getNodeId(); + final DownloadProgressEvent event = new DownloadProgressEvent("tenant", 123L, 10); + event.setNodeId(knownNodeId); + + // test + underTest.distribute(event); + + assertThat(event.getOriginNodeId()).isNull(); + verify(redisTemplateMock, times(0)).convertAndSend(anyString(), eq(event)); + } + + @Test + public void handleDistributedMessageFromRedis() { + final DownloadProgressEvent event = new DownloadProgressEvent("tenant", 123L, 10); + final String knownChannel = "someChannel"; + + underTest.handleMessage(event, knownChannel); + + assertThat(event.getNodeId()).isEqualTo(EventDistributor.getNodeId()); + verify(eventBusMock).post(eq(event)); + } + + @Test + public void handleDistributedMessageFilteredIfSameNodeId() { + final DownloadProgressEvent event = new DownloadProgressEvent("tenant", 123L, 10); + final String knownChannel = "someChannel"; + event.setOriginNodeId(EventDistributor.getNodeId()); + + underTest.handleMessage(event, knownChannel); + + assertThat(event.getNodeId()).isNull(); + verify(eventBusMock, times(0)).post(eq(event)); + } + + @Test + public void subscribedTopicsContains2PatternTopics() { + final Collection topics = underTest.getTopics(); + assertThat(topics).hasSize(1); + } + + private class TestEntityEvent implements EntityEvent { + + private String originNodeId; + private String nodeId; + private final String tenant; + private final MyEntity entity; + + /** + * @param myEntity + */ + public TestEntityEvent(final MyEntity myEntity, final String tenant) { + this.entity = myEntity; + this.tenant = tenant; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.eventbus.event.Event#getRevision() + */ + @Override + public long getRevision() { + return 0; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.eventbus.event.NodeAware#getOriginNodeId() + */ + @Override + public String getOriginNodeId() { + return originNodeId; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.eventbus.event.NodeAware#setOriginNodeId( + * java. lang.String) + */ + @Override + public void setOriginNodeId(final String originNodeId) { + this.originNodeId = originNodeId; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.eventbus.event.NodeAware#getNodeId() + */ + @Override + public String getNodeId() { + return nodeId; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.eventbus.event.NodeAware#setNodeId(java. + * lang. String) + */ + @Override + public void setNodeId(final String nodeId) { + this.nodeId = nodeId; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.eventbus.event.EntityEvent#getEntity(java. + * lang. Class) + */ + @Override + public E getEntity(final Class entityClass) { + return entityClass.cast(entity); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.eventbus.event.EntityEvent#getEntity() + */ + @Override + public Object getEntity() { + return entity; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.eventbus.event.EntityEvent#getTenant() + */ + @Override + public String getTenant() { + return tenant; + } + } + + private class MyEntity implements Identifiable { + private final String id = "123"; + + /* + * (non-Javadoc) + * + * @see org.springframework.hateoas.Identifiable#getId() + */ + @Override + public String getId() { + return id; + } + } + +} diff --git a/hawkbit-cache-redis/src/test/java/org/eclipse/hawkbit/cache/redis/RedisPropertiesTest.java b/hawkbit-cache-redis/src/test/java/org/eclipse/hawkbit/cache/redis/RedisPropertiesTest.java new file mode 100755 index 000000000..6aac1b03f --- /dev/null +++ b/hawkbit-cache-redis/src/test/java/org/eclipse/hawkbit/cache/redis/RedisPropertiesTest.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.cache.redis; + +import static org.fest.assertions.api.Assertions.assertThat; + +import org.eclipse.hawkbit.cache.RedisProperties; +import org.junit.Test; + +public class RedisPropertiesTest { + + @Test + public void setAndGetProps() { + final String knownHost = "bumlux"; + final int knownPort = 1234; + + final RedisProperties underTest = new RedisProperties(); + underTest.setHost(knownHost); + underTest.setPort(knownPort); + + assertThat(underTest.getHost()).isEqualTo(knownHost); + assertThat(underTest.getPort()).isEqualTo(knownPort); + } + +} diff --git a/hawkbit-core/pom.xml b/hawkbit-core/pom.xml new file mode 100755 index 000000000..64dea1bbf --- /dev/null +++ b/hawkbit-core/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + org.eclipse.hawkbit + hawkbit-parent + 0.1.0-SNAPSHOT + + hawkbit-core + hawkBit :: Core + + + + org.springframework.boot + spring-boot + + + org.slf4j + slf4j-api + + + com.google.guava + guava + 18.0 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + ru.yandex.qatools.allure + allure-junit-adaptor + test + + + + \ No newline at end of file diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/ControllerPollProperties.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/ControllerPollProperties.java new file mode 100755 index 000000000..694d5c016 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/ControllerPollProperties.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Defines the polling time for the controllers in HH:MM:SS notation. + * + * + * + */ + +@ConfigurationProperties(prefix = "hawkbit.controller") +public class ControllerPollProperties { + + private String pollingTime = "00:05:00"; + private String pollingOverdueTime = "00:05:00"; + + public String getPollingTime() { + return pollingTime; + } + + public void setPollingTime(final String pollingTime) { + this.pollingTime = pollingTime; + } + + public String getPollingOverdueTime() { + return pollingOverdueTime; + } + + public void setPollingOverdueTime(final String pollingOverdue) { + this.pollingOverdueTime = pollingOverdue; + } + +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/DistributedResourceBundleMessageSource.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/DistributedResourceBundleMessageSource.java new file mode 100755 index 000000000..0b0847652 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/DistributedResourceBundleMessageSource.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import java.io.IOException; +import java.util.Properties; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.support.ReloadableResourceBundleMessageSource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.ResourcePatternResolver; + +/** + * This resource bundles using specified basenames, to resource loading. This + * MessageSource implementation supports more than 1 properties file with the + * same name. All properties files will be merged. + * + * + * + */ +public class DistributedResourceBundleMessageSource extends ReloadableResourceBundleMessageSource { + private static final Logger LOGGER = LoggerFactory.getLogger(DistributedResourceBundleMessageSource.class); + private static final String PROPERTIES_SUFFIX = ".properties"; + private ResourceLoader resourceLoader; + + /** + * Constructor to set Defaults. Default base name is classpath*:/messages. + * So all messages_ will be found. + */ + public DistributedResourceBundleMessageSource() { + setBasename("classpath*:/messages"); + setDefaultEncoding("UTF-8"); + setUseCodeAsDefaultMessage(true); + } + + @Override + protected PropertiesHolder refreshProperties(final String filename, final PropertiesHolder propHolder) { + final Properties properties = new Properties(); + long lastModified = -1; + if (!(resourceLoader instanceof ResourcePatternResolver)) { + LOGGER.warn( + "Resource Loader {} doensn't support getting multiple resources. Default properties mechanism will used", + resourceLoader.getClass().getName()); + return super.refreshProperties(filename, propHolder); + } + + try { + final Resource[] resources = ((ResourcePatternResolver) resourceLoader) + .getResources(filename + PROPERTIES_SUFFIX); + for (final Resource resource : resources) { + final String sourcePath = resource.getURI().toString().replace(PROPERTIES_SUFFIX, ""); + final PropertiesHolder holder = super.refreshProperties(sourcePath, propHolder); + properties.putAll(holder.getProperties()); + if (lastModified < resource.lastModified()) { + lastModified = resource.lastModified(); + } + } + } catch (final IOException ignored) { + LOGGER.warn("Resource with filname " + filename + " couldn't load", ignored); + } + return new PropertiesHolder(properties, lastModified); + } + + @Override + public void setResourceLoader(final ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + super.setResourceLoader(resourceLoader); + } + +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/api/HostnameResolver.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/api/HostnameResolver.java new file mode 100755 index 000000000..09cd1e9df --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/api/HostnameResolver.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.api; + +import java.net.URL; + +/** + * Resolve to reach the server url. + * + * + * + */ +public interface HostnameResolver { + + /** + * + * + * @return + */ + URL resolveHostname(); + +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactRepository.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactRepository.java new file mode 100755 index 000000000..296a566b0 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactRepository.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.artifact.repository; + +import java.io.InputStream; +import java.util.List; + +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash; + +/** + * ArtifactRepository service interface. + * + * + * + */ +public interface ArtifactRepository { + + /** + * Stores an artifact into the repository. + * + * @param tenant + * the tenant to store the artifact + * @param content + * the content to store + * @param filename + * the filename of the artifact + * @param contentType + * the content type of the artifact + * @return the stored artifact + * @throws ArtifactStoreException + * in case storing of the artifact was not successful + */ + DbArtifact store(final InputStream content, final String filename, final String contentType); + + /** + * Stores an artifact into the repository. + * + * @param content + * the content to store + * @param filename + * the filename of the artifact + * @param contentType + * the content type of the artifact + * @param hash + * the hashes of the artifact to do hash-checks after storing the + * artifact, might be {@code null} + * @return the stored artifact + * @throws ArtifactStoreException + * in case storing of the artifact was not successful + * @throws HashNotMatchException + * in case {@code hash} is provided and not matching to the + * calculated hashes during storing + */ + DbArtifact store(final InputStream content, final String filename, final String contentType, DbArtifactHash hash); + + /** + * Deletes an artifact by its ID. + * + * @param artifactId + * the ID of the artifact to delete + */ + void deleteById(final String artifactId); + + /** + * Deletes an artifact by its SHA1 hash. + * + * @param sha1Hash + * the sha1-hash of the artifact to delete + */ + void deleteBySha1(final String sha1Hash); + + /** + * Retrieves a {@link DbArtifact} from the store by it's SHA1 hash. + * + * @param sha1Hash + * the sha1-hash of the file to lookup. + * @return The artifact file object or {@code null} if no file exists. + */ + DbArtifact getArtifactBySha1(String sha1); + + /** + * Retrieves a {@link DbArtifact} from the store by it's ID. + * + * @param id + * the ID of the artifact to retrieve + * @return The artifact file object or {@code null} if no file exists. + */ + DbArtifact getArtifactById(final String id); + + /** + * Retrieves a list of {@link GridFSDBFile} from the store by all SHA1 + * hashes. + * + * @param tenant + * the tenant to retrieve the artifacts from, ignore case. + * @param sha1Hashes + * the sha1-hashes of the files to lookup. + * @return list of artfiacts + */ + List getArtifactsBySha1(final List sha1Hashes); + +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactStoreException.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactStoreException.java new file mode 100755 index 000000000..ba41e72a5 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactStoreException.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.artifact.repository; + +/** + * {@link ArtifactStoreException} is thrown in case storing of an artifact was + * not successful. + * + * + * + */ +public class ArtifactStoreException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * Constructs a ArtifactStoreException with message and cause. + * + * @param message + * the message of the exception + * @param cause + * the cause of the exception + */ + public ArtifactStoreException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * Constructs a ArtifactStoreException with message. + * + * @param message + * the message of the exception + */ + public ArtifactStoreException(final String message) { + super(message); + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/HashNotMatchException.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/HashNotMatchException.java new file mode 100755 index 000000000..0c15d817e --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/HashNotMatchException.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.artifact.repository; + +/** + * Thrown when provided hashes and hashes caluclated during storing are not + * matching. + * + * + * + */ +public class HashNotMatchException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public static final String SHA1 = "SHA-1"; + public static final String MD5 = "MD5"; + + private final String hashFunction; + + /** + * Constructs a HashNotMatchException with message and cause. + * + * @param message + * the message of the exception + * @param cause + * the cause of the exception + * @param hashFunction + * the hash function which caused this exception + */ + public HashNotMatchException(final String message, final Throwable cause, final String hashFunction) { + super(message, cause); + this.hashFunction = hashFunction; + } + + /** + * Constructs a HashNotMatchException with message. + * + * @param message + * the message of the exception + * @param hashFunction + * the hash function which caused this exception + */ + public HashNotMatchException(final String message, final String hashFunction) { + super(message); + this.hashFunction = hashFunction; + } + + /** + * @return the hashFunction + */ + public String getHashFunction() { + return hashFunction; + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/model/DbArtifact.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/model/DbArtifact.java new file mode 100755 index 000000000..b4c2dcdba --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/model/DbArtifact.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.artifact.repository.model; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Database representation of artifact. + * + * + * + * + */ +public class DbArtifact { + + private String artifactId; + + private DbArtifactHash hashes; + + private Long size; + + private String contentType; + + private InputStream fileInputStream; + + private OutputStream fileOutputStream; + + public void setArtifactId(final String artifactId) { + this.artifactId = artifactId; + } + + public String getArtifactId() { + return artifactId; + } + + public DbArtifactHash getHashes() { + return hashes; + } + + public void setHashes(final DbArtifactHash hashes) { + this.hashes = hashes; + } + + public Long getSize() { + return size; + } + + public void setSize(final Long size) { + this.size = size; + } + + public void setContentType(final String contentType) { + this.contentType = contentType; + } + + public String getContentType() { + return contentType; + } + + public void setFileInputStream(final InputStream fileInputStream) { + this.fileInputStream = fileInputStream; + } + + public InputStream getFileInputStream() { + return fileInputStream; + } + + public OutputStream getFileOutputStream() { + return fileOutputStream; + } + + public void setFileOutputStream(final OutputStream fileOutputStream) { + this.fileOutputStream = fileOutputStream; + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/model/DbArtifactHash.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/model/DbArtifactHash.java new file mode 100755 index 000000000..c16d3a620 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/model/DbArtifactHash.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.artifact.repository.model; + +/** + * Database representation of artifact hash. + * + * + * + * + */ +public class DbArtifactHash { + + private String sha1; + + private String md5; + + /** + * Constructor. + * + * @param sha1 + * the sha1 hash + * @param md5 + * the md5 hash + */ + public DbArtifactHash(final String sha1, final String md5) { + super(); + this.sha1 = sha1; + this.md5 = md5; + } + + public void setSha1(final String sha1) { + this.sha1 = sha1; + } + + public void setMd5(final String md5) { + this.md5 = md5; + } + + public String getSha1() { + return sha1; + } + + public String getMd5() { + return md5; + } + +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/cache/CacheConstants.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/cache/CacheConstants.java new file mode 100755 index 000000000..6bc28d7f3 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/cache/CacheConstants.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.cache; + +/** + * Cache Constants. + * + * + * + */ +public final class CacheConstants { + + /** + * Constant for download cache id. + */ + public static final String DOWNLOAD_ID_CACHE = "DowonloadIdCache"; + + private CacheConstants() { + } + +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/cache/DownloadArtifactCache.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/cache/DownloadArtifactCache.java new file mode 100755 index 000000000..b533b0058 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/cache/DownloadArtifactCache.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.cache; + +/** + * Cache Object for download a Artifact. + * + * + * + */ +public class DownloadArtifactCache { + + private final DownloadType downloadType; + private final String id; + + /** + * Constructor. + * + * @param downloadType + * the type for searching the artifact. + * @param id + * the searching id e.g. sha1, md5 + */ + public DownloadArtifactCache(final DownloadType downloadType, final String id) { + super(); + this.downloadType = downloadType; + this.id = id; + } + + public String getId() { + return id; + } + + public DownloadType getDownloadType() { + return downloadType; + } + +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/cache/DownloadType.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/cache/DownloadType.java new file mode 100755 index 000000000..4a78f3e4c --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/cache/DownloadType.java @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.cache; + +/** + * The type of the id which is saved. + * + * + * + */ +public enum DownloadType { + BY_SHA1 +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/cache/TenancyCacheManager.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/cache/TenancyCacheManager.java new file mode 100755 index 000000000..5b58ea53e --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/cache/TenancyCacheManager.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.cache; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +/** + * + * + */ +public interface TenancyCacheManager extends CacheManager { + + /** + * A direct access for retrieving the cache without including the current + * tenant key. This is necessary e.g. for retrieving caches not for the + * current tenant. + * + * @param name + * the name of the cache to retrieve directly + * @return the cache associated with the name without tenancy separation + */ + Cache getDirectCache(String name); + + /** + * Evicts all caches for a given tenant. All caches under a certain tenant + * gets evicted. + * + * @param tenant + * the tenant to evict caches + */ + void evictCaches(final String tenant); + +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/cache/TenantAwareCacheManager.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/cache/TenantAwareCacheManager.java new file mode 100755 index 000000000..06d6e1719 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/cache/TenantAwareCacheManager.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.cache; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +/** + * A {@link CacheManager} delegator which wraps the + * {@link CacheManager#getCache(String)} and + * {@link CacheManager#getCacheNames()} to include the + * {@link TenantAware#getCurrentTenant()} when accessing a cache, so caches are + * seperated. + * + * Additionally it also provide functionality to retrieve all caches overall + * tenants at once, for monitoring and system access. + * + * + * + * + */ +public class TenantAwareCacheManager implements TenancyCacheManager { + + private static final String TENANT_CACHE_DELIMITER = "|"; + + private final CacheManager delegate; + + private final TenantAware tenantAware; + + /** + * @param delegate + * the {@link CacheManager} to delegate to. + * @param tenantAware + * the tenant aware to retrieve the current tenant + */ + public TenantAwareCacheManager(final CacheManager delegate, final TenantAware tenantAware) { + this.delegate = delegate; + this.tenantAware = tenantAware; + } + + @Override + public Cache getCache(final String name) { + final String currentTenant = tenantAware.getCurrentTenant().toUpperCase(); + if (currentTenant.contains(TENANT_CACHE_DELIMITER)) { + return null; + } + return delegate.getCache(currentTenant + TENANT_CACHE_DELIMITER + name); + } + + @Override + public Collection getCacheNames() { + final String currentTenant = tenantAware.getCurrentTenant().toUpperCase(); + if (currentTenant.contains(TENANT_CACHE_DELIMITER)) { + return Collections.emptyList(); + } + return getCacheNames(currentTenant); + } + + /** + * A direct access for retrieving all cache names overall tenants. + * + * @return all cache names without tenant check + */ + public Collection getDirectCacheNames() { + return delegate.getCacheNames(); + } + + @Override + public Cache getDirectCache(final String name) { + return delegate.getCache(name); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.cache.TenancyCacheManager#evictCaches(java. + * lang. String) + */ + @Override + public void evictCaches(final String tenant) { + getCacheNames(tenant) + .forEach(cachename -> delegate.getCache(tenant + TENANT_CACHE_DELIMITER + cachename).clear()); + } + + private Collection getCacheNames(final String tenant) { + final String tenantWithDelimiter = tenant + TENANT_CACHE_DELIMITER; + return delegate.getCacheNames().parallelStream().filter(cacheName -> cacheName.startsWith(tenantWithDelimiter)) + .map(cacheName -> cacheName.substring(tenantWithDelimiter.length())).collect(Collectors.toList()); + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/DeadEventListener.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/DeadEventListener.java new file mode 100755 index 000000000..cc717efb3 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/DeadEventListener.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.eventbus.DeadEvent; +import com.google.common.eventbus.Subscribe; + +/** + * Catches all dead events by means of events with no fitting subscriber on the + * bus. + * + * + * + */ +@EventSubscriber +public class DeadEventListener { + private static final Logger LOG = LoggerFactory.getLogger(DeadEventListener.class); + + /** + * Listens for dead vents and prints them into LOG. + * + * @param event + * to print + */ + @Subscribe + public void listen(final DeadEvent event) { + LOG.info("DeadEvent on bus! {}", event.getEvent()); + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/EventBusSubscriberProcessor.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/EventBusSubscriberProcessor.java new file mode 100755 index 000000000..f7ce86ef6 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/EventBusSubscriberProcessor.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus; + +import java.lang.reflect.Method; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; + +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; + +/** + * An {@link BeanPostProcessor} implementation which registers all beans as a + * event bus subscriber if the classes are annotated with + * {@link EventSubscriber} and have at least one method annotated with the + * guava's {@link Subscribe} annoation. + * + * + */ + +public class EventBusSubscriberProcessor implements BeanPostProcessor { + + private static final Logger LOGGER = LoggerFactory.getLogger(EventBusSubscriberProcessor.class); + + @Autowired + private EventBus eventBus; + + /* + * (non-Javadoc) + * + * @see org.springframework.beans.factory.config.BeanPostProcessor# + * postProcessBeforeInitialization (java.lang.Object, java.lang.String) + */ + @Override + public Object postProcessBeforeInitialization(final Object bean, final String beanName) { + return bean; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.beans.factory.config.BeanPostProcessor# + * postProcessAfterInitialization( java.lang.Object, java.lang.String) + */ + @Override + public Object postProcessAfterInitialization(final Object bean, final String beanName) { + final Class beanClass = bean.getClass(); + final EventSubscriber eventSubscriber = beanClass.getAnnotation(EventSubscriber.class); + if (eventSubscriber != null) { + LOGGER.trace("Found bean {} with {} annotation ", bean.getClass().getName(), + EventSubscriber.class.getSimpleName()); + final Method[] declaredMethods = beanClass.getDeclaredMethods(); + for (final Method method : declaredMethods) { + final Subscribe subscriber = method.getAnnotation(Subscribe.class); + if (subscriber != null) { + LOGGER.trace("Found method {} for bean {} with {} annotation", method.getName(), + bean.getClass().getName(), Subscribe.class.getSimpleName()); + eventBus.register(bean); + return bean; + } + } + } + if (eventSubscriber != null) { + LOGGER.debug("Found bean {} with {} annotation but without any method with necessary {} annotation", + bean.getClass().getName(), EventSubscriber.class.getSimpleName(), Subscribe.class.getSimpleName()); + } + return bean; + } + + /** + * package private setter for testing purposes. + * + * @param eventBus + * the event bus + */ + void setEventBus(final EventBus eventBus) { + this.eventBus = eventBus; + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/EventSubscriber.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/EventSubscriber.java new file mode 100755 index 000000000..2a30d32b1 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/EventSubscriber.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Service; + +/** + * Marks an class as an event subscriber to listen on event on the event bus + * without explicit register this class to the event bus. + * + *
+ * @EventSubscriber
+ * public class MySubscriber {
+ *     @Subscribe
+ *     public void listen(MyEvent event) {
+ *         System.out.println("event received: " + event);
+ *     }
+ * }
+ * 
+ * + * + * + * + */ +@Target({ TYPE }) +@Retention(RUNTIME) +@Service +public @interface EventSubscriber { + +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/AbstractDistributedEvent.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/AbstractDistributedEvent.java new file mode 100755 index 000000000..c467a746c --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/AbstractDistributedEvent.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus.event; + +/** + * An abstract class of the {@link DistributedEvent} implementation which holds + * all the necessary information of distributing events to other nodes. + * + * + * + * + */ +public abstract class AbstractDistributedEvent implements DistributedEvent { + + private static final long serialVersionUID = 1L; + private final long revision; + private String originNodeId; + private String nodeId; + private final String tenant; + + /** + * + * @param revision + * the revision of this event + * @param tenant + * the tenant for this event + */ + protected AbstractDistributedEvent(final long revision, final String tenant) { + this.revision = revision; + this.tenant = tenant; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.eventbus.event.NodeAware#setOriginNodeId(java. + * lang. String) + */ + @Override + public void setOriginNodeId(final String originNodeId) { + this.originNodeId = originNodeId; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.eventbus.event.NodeAware#setNodeId(java.lang. + * String) + */ + @Override + public void setNodeId(final String nodeId) { + this.nodeId = nodeId; + + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.eventbus.event.NodeAware#getOriginNodeId() + */ + @Override + public String getOriginNodeId() { + return this.originNodeId; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.eventbus.event.NodeAware#getNodeId() + */ + @Override + public String getNodeId() { + return this.nodeId; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.eventbus.event.Event#getRevision() + */ + @Override + public long getRevision() { + return revision; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.eventbus.event.EntityEvent#getTenant() + */ + @Override + public String getTenant() { + return tenant; + } + +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/CancelTargetAssignmentEvent.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/CancelTargetAssignmentEvent.java new file mode 100755 index 000000000..ad421b266 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/CancelTargetAssignmentEvent.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus.event; + +import java.net.URI; + +/** + * Event that gets sent when the assignment of a distribution set to a target + * gets canceled. + * + * + * + */ +public class CancelTargetAssignmentEvent { + + private final String controllerId; + private final Long actionId; + private final URI targetAdress; + + /** + * Creates a new {@link CancelTargetAssignmentEvent}. + * + * @param controllerId + * the ID of the controller + * @param actionId + * the action id of the assignment + * @param targetAdress + * the targetAdress of the target + */ + public CancelTargetAssignmentEvent(final String controllerId, final Long actionId, final URI targetAdress) { + this.controllerId = controllerId; + this.actionId = actionId; + this.targetAdress = targetAdress; + } + + /** + * @return the action id of the assignment + */ + public Long getActionId() { + return actionId; + } + + /** + * @return the controllerId of the Target which has been assigned to the + * distribution set + */ + public String getControllerId() { + return controllerId; + } + + /** + * + * @return the targetr adress. + */ + public URI getTargetAdress() { + return targetAdress; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "TargetAssignDistributionSetEvent [targetAdress=" + targetAdress + ", controllerId=" + controllerId + + ", actionId=" + actionId + "]"; + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/CustomEvents.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/CustomEvents.java new file mode 100755 index 000000000..4416e1a63 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/CustomEvents.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus.event; + +/** + * Events to be published to refresh data on UI. + * + * + * + * + */ +public enum CustomEvents { + + TARGETS_CREATED_EVENT, + + DISTRIBUTION_CREATED_EVENT + +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/DistributedEvent.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/DistributedEvent.java new file mode 100755 index 000000000..b529afa91 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/DistributedEvent.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus.event; + +import java.io.Serializable; + +/** + * Marks an event to as an distributed event which will be distributed to other + * nodes. + * + * + * + * + */ +public interface DistributedEvent extends Event, NodeAware, Serializable { + +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/DownloadProgressEvent.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/DownloadProgressEvent.java new file mode 100755 index 000000000..74679f02e --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/DownloadProgressEvent.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus.event; + +/** + * Event that contains an updated download progress for a given Action. + * + * + * + * + */ +public class DownloadProgressEvent extends AbstractDistributedEvent { + + private static final long serialVersionUID = 1L; + + private final Long statusId; + private final int progressPercent; + + /** + * Constructor. + * + * @param tenant + * the tenant for this event + * @param statusId + * of {@link UpdateActionStatus} + * @param progressPercent + * number (1-100) + */ + public DownloadProgressEvent(final String tenant, final Long statusId, final int progressPercent) { + // the revision of the DownloadProgressEvent is just equal the + // progressPercentage due the + // percentage is going from 0 to 100. + super(statusId, tenant); + this.statusId = statusId; + this.progressPercent = progressPercent; + } + + /** + * @return the statusId + */ + public Long getStatusId() { + return statusId; + } + + /** + * @return the progressPercent + */ + public int getProgressPercent() { + return progressPercent; + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/EntityEvent.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/EntityEvent.java new file mode 100755 index 000000000..688b261e1 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/EntityEvent.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus.event; + +/** + * An event interface which declares event types that an entity has been + * changed. {@link EntityEvent}s should not implement {@link DistributedEvent} + * due all {@link EntityEvent}s will be distributed to other nodes. + * + * Retrieving an {@link EntityEvent} on a different node the entity will be load + * lazy. + * + * + * + * + */ +public interface EntityEvent extends Event, NodeAware { + + /** + * A typesafe way to retrieve the entity from the event, which might be + * loaded lazy in case the event has been distributed from another node. + * + * @param entityClass + * the class of the entity to retrieve + * @return the entity might be lazy loaded. Might be {@code null} in case + * the entity e.g. is queried lazy on a different node and has been + * already deleted from the database + * @throws ClassCastException + * in case a wrong entity class is given for this event + */ + E getEntity(Class entityClass); + + /** + * An unsafe way to retrieve the entity from this event which might be + * loaded lazy in case the event has been distributed from another node. + * + * @return the entity might be lazy loaded. Might be {@code null} in case + * the entity e.g. is queried lazy on a different node and has been + * already deleted from the database + */ + Object getEntity(); +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/Event.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/Event.java new file mode 100755 index 000000000..9cbf6ef12 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/Event.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus.event; + +/** + * An event declaration which holds an revision for each event so consumers have + * the chance to know if they might already retrieved an newer event. + * + * + * + * + */ +public interface Event { + + /** + * @return the revision of this event which should be increment or each new + * event in case the event have a causalität. Might be {@code -1} in + * case the events does not provide any revision. + */ + long getRevision(); + + /** + * @return the tenant of the entity. + */ + String getTenant(); +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/NodeAware.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/NodeAware.java new file mode 100755 index 000000000..2d7532d07 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/NodeAware.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus.event; + +/** + * Interface to be implemented by events which are distributed to other nodes + * where it's necessary to know which node sent an event and which nodes + * retrieved the event from which node. Using the EventDistributor the + * implementation only needs to contain the necessary node IDs, setting and + * retrieving the node IDs is transparent by the EventDistributor so the event + * distributor does not hang in an endless loop of distributing the events which + * self posted. + * + * + * + * + */ +public interface NodeAware { + + /** + * @return the origin node ID in case the event has been forwarded to other + * nodes or {@code null} if the event has not been forwarded to + * other nodes + */ + String getOriginNodeId(); + + /** + * @param originNodeId + * the origin node ID where this event has been sent originally + */ + void setOriginNodeId(String originNodeId); + + /** + * @return the node ID which is processing this event locally, set by the + * EventDistributor so he can determine if this event has been + * received by another node and is processing on the current node. + */ + String getNodeId(); + + /** + * @param nodeId + * the ID of the node this event is processing. + */ + void setNodeId(String nodeId); + +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/TargetDeletedEvent.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/TargetDeletedEvent.java new file mode 100755 index 000000000..e6fcac135 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/TargetDeletedEvent.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus.event; + +/** + * + * + * + */ +public class TargetDeletedEvent extends AbstractDistributedEvent { + + private static final long serialVersionUID = 1L; + private final long targetId; + + /** + * @param tenant + * the tenant for this event + * @param targetId + * the ID of the target which has been deleted + */ + public TargetDeletedEvent(final String tenant, final long targetId) { + super(-1, tenant); + this.targetId = targetId; + } + + /** + * @return the targetId + */ + public long getTargetId() { + return targetId; + } + +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/GenericSpServerException.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/GenericSpServerException.java new file mode 100755 index 000000000..f0a3f4d1f --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/GenericSpServerException.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.exception; + +/** + * {@link GenericSpServerException} is thrown when a given entity in's actual + * and cannot be stored within the current session. Reason could be that it has + * been changed within another session. + * + * + * + */ +public class GenericSpServerException extends SpServerRtException { + private static final long serialVersionUID = 1L; + private static final SpServerError THIS_ERROR = SpServerError.SP_REPO_GENERIC_ERROR; + + /** + * Constructor. + */ + public GenericSpServerException() { + super(THIS_ERROR); + } + + /** + * Parameterized constructor. + * + * @param cause + * of the exception + */ + public GenericSpServerException(final Throwable cause) { + super(THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * custom error message + * @param cause + * of the exception + */ + public GenericSpServerException(final String message, final Throwable cause) { + super(message, THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * custom error message + */ + public GenericSpServerException(final String message) { + super(message, THIS_ERROR); + } + +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java new file mode 100755 index 000000000..c7996f0dd --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java @@ -0,0 +1,192 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.exception; + +/** + * + * + * + * Define the Error code for Error handling + * + */ + +public enum SpServerError { + /** + * + */ + SP_REPO_GENERIC_ERROR("hawkbit.server.error.repo.genericError", "unknown error occured"), + /** + * + */ + SP_REPO_ENTITY_ALRREADY_EXISTS("hawkbit.server.error.repo.entitiyAlreayExists", + "The given entity already exists in database"), + /** + * + */ + SP_REPO_ENTITY_NOT_EXISTS("hawkbit.server.error.repo.entitiyNotFound", + "The given entity does not exist in database"), + /** + * + */ + SP_REPO_CONCURRENT_MODIFICATION("hawkbit.server.error.repo.concurrentModification", + "The given entity has been changed by another user/session"), + /** + * + */ + SP_REST_SORT_PARAM_SYNTAX("hawkbit.server.error.rest.param.sortParamSyntax", + "The given sort paramter is not well formed"), + + /** + * + */ + SP_REST_RSQL_SEARCH_PARAM_SYNTAX("hawkbit.server.error.rest.param.rsqlParamSyntax", + "The given search paramter is not well formed"), + + /** + * + */ + SP_REST_RSQL_PARAM_INVALID_FIELD("hawkbit.server.error.rest.param.rsqlInvalidField", + "The given search parameter field does not exist"), + + /** + * + */ + SP_REST_SORT_PARAM_INVALID_FIELD("hawkbit.server.error.rest.param.invalidField", + "The given sort parameter field does not exist"), + + /** + * + */ + SP_REST_SORT_PARAM_INVALID_DIRECTION("hawkbit.server.error.rest.param.invalidDirection", + "The given sort parameter direction does not exist"), + /** + * + */ + SP_REST_BODY_NOT_READABLE("hawkbit.server.error.rest.body.notReadable", + "The given request body is not well formed"), SP_UI_SEARCH("hawkbit.server.error.ui.searchError", + "blabla :-)"), + /** + * + */ + SP_ARTIFACT_UPLOAD_FAILED("hawkbit.server.error.artifact.uploadFailed", + "Upload of artifact failed with internal server error."), + + /** + * + */ + SP_ARTIFACT_UPLOAD_FAILED_MD5_MATCH("hawkbit.server.error.artifact.uploadFailed.checksum.md5.match", + "Upload of artifact failed as the provided MD5 checksum did not match with the provided artifact."), + + /** + * + */ + SP_ARTIFACT_UPLOAD_FAILED_SHA1_MATCH("hawkbit.server.error.artifact.uploadFailed.checksum.sha1.match", + "Upload of artifact failed as the provided SHA1 checksum did not match with the provided artifact."), + + /** + * + */ + SP_DS_CREATION_FAILED_MISSING_MODULE("hawkbit.server.error.distributionset.creationFailed.missingModule", + "Creation if Distribution Set failed as module is missing that is configured as mandatory."), + + /** + * + */ + SP_ARTIFACT_UPLOAD_FILE_LIMIT_EXCEEDED("hawkbit.server.error.artifact.uploadFailed.sizelimitexceeded", + "Upload of artifact failed as the file exceeds its maximum permitted size"), + /** + * + */ + SP_INSUFFICIENT_PERMISSION("hawkbit.server.error.insufficientpermission", "Insufficient Permission"), + + /** + * + */ + SP_ARTIFACT_DELETE_FAILED("hawkbit.server.error.artifact.deleteFailed", + "Deletion of artifact failed with internal server error."), + /** + * + */ + + SP_ARTIFACT_LOAD_FAILED("hawkbit.server.error.artifact.loadFailed", + "Load of artifact failed with internal server error."), + + /** + * + */ + SP_ACTION_STATUS_TO_MANY_ENTRIES("hawkbit.server.error.action.status.tooManyEntries", + "Too many status entries have been inserted."), + + /** + * + */ + SP_ATTRIBUTES_TO_MANY_ENTRIES("hawkbit.server.error.target.attributes.tooManyEntries", + "Too many attribute entries have been inserted."), + + /** + * + */ + SP_ACTION_NOT_CANCELABLE("hawkbit.server.error.action.notcancelable", + "Only active actions which are in status pending are canceable"), + + /** + * + */ + SP_DS_INCOMPLETE("hawkbit.server.error.distributionset.incomplete", + "Distribution set is assigned to a a target that is incomplete (i.e. mandatory modules are missing)"), + + /** + * + */ + SP_DS_TYPE_UNDEFINED("hawkbit.server.error.distributionset.type.undefined", + "Distribution set type is not yet defined. Modules cannot be added until definition."), + + /** + * + */ + SP_DS_MODULE_UNSUPPORTED("hawkbit.server.error.distributionset.modules.unsupported", + "Distribution set type does not contain the given module, i.e. is incompatible."), + + /** + * + */ + SP_REPO_TENANT_NOT_EXISTS("hawkbit.server.error.repo.tenantNotExists", + "The entity cannot be inserted due the tenant does not exists"), + + /** + * + */ + SP_ENTITY_LOCKED("hawkbit.server.error.entitiylocked", "The given entity is locked by the server."), + + /** + * + */ + SP_REPO_ENTITY_READ_ONLY("hawkbit.server.error.entityreadonly", + "The given entity is read only and the change cannot be completed."); + + private final String key; + private final String message; + + /* + * Repository side Error codes + */ + private SpServerError(final String errorKey, final String message) { + key = errorKey; + this.message = message; + } + + public String getKey() { + return key; + } + + public String getMessage() { + return message; + } + +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerRtException.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerRtException.java new file mode 100755 index 000000000..1e316f562 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerRtException.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.exception; + +/** + * + * + * + * Generic Custom Exception to wrap the Runtime and checked exception + * + */ + +public abstract class SpServerRtException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final SpServerError error; + + /** + * Parameterized constructor. + * + * @param error + * detail + */ + public SpServerRtException(final SpServerError error) { + super(error.getMessage()); + this.error = error; + } + + /** + * Parameterized constructor. + * + * @param message + * custom error message + * @param error + * detail + */ + public SpServerRtException(final String message, final SpServerError error) { + super(message); + this.error = error; + } + + /** + * Parameterized constructor. + * + * @param message + * custom error message + * @param error + * detail + * @param cause + * of the exception + */ + public SpServerRtException(final String message, final SpServerError error, final Throwable cause) { + super(message, cause); + this.error = error; + } + + /** + * Parameterized constructor. + * + * @param error + * detail + * @param cause + * of the exception + */ + public SpServerRtException(final SpServerError error, final Throwable cause) { + super(error.getMessage(), cause); + this.error = error; + } + + public SpServerError getError() { + return error; + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionFields.java new file mode 100755 index 000000000..bc153d989 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionFields.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +/** + * Sort fields for {@link ActionRest}. + * + * + * + * + */ +public enum ActionFields implements FieldNameProvider, FieldValueConverter { + + /** + * The status field. + */ + STATUS("active"), + /** + * The id field. + */ + ID("id"); + + private static final String ACTIVE = "pending"; + private static final String INACTIVE = "finished"; + + private final String fieldName; + + private ActionFields(final String fieldName) { + this.fieldName = fieldName; + } + + @Override + public String getFieldName() { + return fieldName; + } + + @Override + public Object convertValue(final ActionFields e, final String value) { + switch (e) { + case STATUS: + return convertStatusValue(value); + + default: + return value; + } + } + + private Object convertStatusValue(final String value) { + final String trimmedValue = value.trim(); + if (trimmedValue.equalsIgnoreCase(ACTIVE)) { + return 1; + } else if (trimmedValue.equalsIgnoreCase(INACTIVE)) { + return 0; + } else { + return null; + } + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.rest.resource.model.FieldValueConverter# + * possibleValues(java.lang.Enum) + */ + @Override + public String[] possibleValues(final ActionFields e) { + switch (e) { + case STATUS: + return new String[] { ACTIVE, INACTIVE }; + default: + return new String[0]; + } + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionStatusFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionStatusFields.java new file mode 100755 index 000000000..f1aa7a8e0 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionStatusFields.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +/** + * Sort fields for {@link ActionStatusRest}. + * + * + * + * + */ +public enum ActionStatusFields implements FieldNameProvider { + + /** + * The type field. + */ + TYPE("type"), + /** + * The id field. + */ + ID("id"); + + private final String fieldName; + + private ActionStatusFields(final String fieldName) { + this.fieldName = fieldName; + } + + @Override + public String getFieldName() { + return fieldName; + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFields.java new file mode 100755 index 000000000..6935ed051 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFields.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +/** + * Describing the fields of the DistributionSet model which can be used in the + * REST API e.g. for sorting etc. + * + * + * + * + */ +public enum DistributionSetFields implements FieldNameProvider { + /** + * The name field. + */ + NAME("name"), + /** + * The description field. + */ + DESCRIPTION("description"), + /** + * The version field. + */ + VERSION("version"), + /** + * The complete field. + */ + COMPLETE("complete"), + /** + * The id field. + */ + ID("id"); + + private final String fieldName; + + private DistributionSetFields(final String fieldName) { + this.fieldName = fieldName; + } + + @Override + public String getFieldName() { + return fieldName; + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetMetadataFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetMetadataFields.java new file mode 100755 index 000000000..8915e3725 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetMetadataFields.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +/** + * Sort fields for DistributionSetMetadata. + * + * + * + * + */ +public enum DistributionSetMetadataFields implements FieldNameProvider { + + /** + * The value field. + */ + VALUE("value"), + /** + * The key field. + */ + KEY("key"); + + private final String fieldName; + + private DistributionSetMetadataFields(final String fieldName) { + this.fieldName = fieldName; + } + + @Override + public String getFieldName() { + return fieldName; + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTypeFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTypeFields.java new file mode 100755 index 000000000..f8e9e8671 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTypeFields.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +/** + * Describing the fields of the DistributionSetType model which can be used in + * the REST API e.g. for sorting etc. + * + * + * + * + */ +public enum DistributionSetTypeFields implements FieldNameProvider { + /** + * The name field. + */ + NAME("name"), + /** + * The description field. + */ + DESCRIPTION("description"), + + /** + * The type key field. + */ + KEY("key"), + + /** + * The id field. + */ + ID("id"); + + private final String fieldName; + + private DistributionSetTypeFields(final String fieldName) { + this.fieldName = fieldName; + } + + @Override + public String getFieldName() { + return fieldName; + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/FieldNameProvider.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/FieldNameProvider.java new file mode 100755 index 000000000..c2ad31a85 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/FieldNameProvider.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +/** + * An interface for declaring the name of the field described in the database + * which is used as string representation of the field, e.g. for sorting the + * fields over REST. + * + * + * + */ +public interface FieldNameProvider { + + /** + * @return the string representation of the underlying persistence field + * name e.g. in case of sorting. Never {@code null}. + */ + String getFieldName(); +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/FieldValueConverter.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/FieldValueConverter.java new file mode 100755 index 000000000..b8b83de6f --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/FieldValueConverter.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +/** + * A value convert which converts given string based values into an object which + * can be used for building generic queries. Mapping external API values e.g. + * REST API to inside representation on database. E.g. mapping 'pending' or + * 'finished' values in rest queries to {@link TargetAction#isActive()} boolean + * value. + * + * + * + * @param + * the enum parameter + * + */ +public interface FieldValueConverter> { + + /** + * converts the given {@code value} into the representation to build a + * generic query. + * + * @param e + * the enum to build the value for + * @param value + * the value in string representation + * @return the converted object or {@code null} if conversation fails, if + * given enum does not need to be converted the the unmodified + * {@code value} is returned. + */ + Object convertValue(final T e, final String value); + + /** + * returns the possible values associated with the given enum type. + * + * @param e + * the enum type to retrieve the possible values + * @return the possible values for a specific enum or {@code null} + */ + String[] possibleValues(final T e); + +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleFields.java new file mode 100755 index 000000000..0425f8c03 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleFields.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +/** + * Describing the fields of the SoftwareModule model which can be used in the + * REST API e.g. for sorting etc. + * + * + * + * + */ +public enum SoftwareModuleFields implements FieldNameProvider { + /** + * The name field. + */ + NAME("name"), + /** + * The description field. + */ + DESCRIPTION("description"), + /** + * The description field. + */ + VERSION("version"), + /** + * The type field of the software module. + */ + TYPE("type.key"), + /** + * The id field. + */ + ID("id"); + + private final String fieldName; + + private SoftwareModuleFields(final String fieldName) { + this.fieldName = fieldName; + } + + @Override + public String getFieldName() { + return fieldName; + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleMetadataFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleMetadataFields.java new file mode 100755 index 000000000..8286170cd --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleMetadataFields.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +/** + * Sort fields for SoftwareModuleMetadata. + * + * + * + * + */ +public enum SoftwareModuleMetadataFields implements FieldNameProvider { + + /** + * The value field. + */ + VALUE("value"), + /** + * The key field. + */ + KEY("key"); + + private final String fieldName; + + private SoftwareModuleMetadataFields(final String fieldName) { + this.fieldName = fieldName; + } + + @Override + public String getFieldName() { + return fieldName; + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleTypeFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleTypeFields.java new file mode 100755 index 000000000..beb46a9d7 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleTypeFields.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +/** + * Describing the fields of the SoftwareModuleType model which can be used in + * the REST API e.g. for sorting etc. + * + * + * + * + */ +public enum SoftwareModuleTypeFields implements FieldNameProvider { + /** + * The name field. + */ + NAME("name"), + /** + * The description field. + */ + DESCRIPTION("description"), + /** + * The type key field. + */ + KEY("key"), + /** + * The id field. + */ + ID("id"), + /** + * The max ds assignments field. + */ + MAX("maxAssignments"); + + private final String fieldName; + + private SoftwareModuleTypeFields(final String fieldName) { + this.fieldName = fieldName; + } + + @Override + public String getFieldName() { + return fieldName; + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java new file mode 100755 index 000000000..1ffc04fc3 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +/** + * Describing the fields of the Target model which can be used in the REST API + * e.g. for sorting etc. + * + * + * + * + */ +public enum TargetFields implements FieldNameProvider { + /** + * The name field. + */ + NAME("name"), + /** + * The description field. + */ + DESCRIPTION("description"), + /** + * The controllerId field. + */ + CONTROLLERID("controllerId"), + /** + * The updateStatus field. + */ + UPDATESTATUS("targetInfo.updateStatus"), + + /** + * The ip-address field. + */ + IPADDRESS("targetInfo.ipAddress"); + + private final String fieldName; + + private TargetFields(final String fieldName) { + this.fieldName = fieldName; + } + + @Override + public String getFieldName() { + return fieldName; + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/tenancy/TenantAware.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/tenancy/TenantAware.java new file mode 100755 index 000000000..45aef3890 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/tenancy/TenantAware.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.tenancy; + +/** + * Interface for components that are aware of the application's current tenant. + * + * + * + * + */ +public interface TenantAware { + + /** + * Implementation might retrieve the current tenant from a session or + * thread-local. + * + * @return the current tenant + */ + String getCurrentTenant(); + + /** + * Gives the possibility to run a certain code under a specific given + * {@code tenant}. Only the given {@link TenantRunner} is executed under the + * specific tenant e.g. under control of an {@link ThreadLocal}. After the + * {@link TenantRunner} it must be ensured that the original tenant before + * this invocation is reset. + * + * @param tenant + * the tenant which the specific code should run + * @param tenantRunner + * the runner which is implemented to run this specific code + * under the given tenant + * @return the return type of the {@link TenantRunner} + * @throws any + * kind of {@link RuntimeException} + */ + T runAsTenant(final String tenant, TenantRunner tenantRunner); + + /** + * An {@link TenantRunner} interface which allows to run specific code under + * a given tenant by using the + * {@link TenantAware#runAsTenant(String, TenantRunner)}. + * + * + * + * + * @param + * the return type of the runner + */ + interface TenantRunner { + /** + * Called to run specific code and a given tenant. + * + * @return the return of the code block running under a certain tenant + */ + T run(); + } +} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationKey.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationKey.java new file mode 100755 index 000000000..477cd654e --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationKey.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.tenancy.configuration; + +/** + * An enum which defines the tenant specific configurations which can be + * configured for each tenant seperately. + * + * + * + * + */ +public enum TenantConfigurationKey { + + /** + * boolean value {@code true} {@code false}. + */ + AUTHENTICATION_MODE_HEADER_ENABLED("authentication.header.enabled", + "hawkbit.server.controller.security.authentication.header.enabled", Boolean.FALSE.toString()), + /** + * + */ + AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME("authentication.header.authority", + "hawkbit.server.controller.security.authentication.header.authority", Boolean.FALSE.toString()), + /** + * boolean value {@code true} {@code false}. + */ + AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED("authentication.targettoken.enabled", + "hawkbit.server.controller.security.authentication.targettoken.enabled", Boolean.FALSE.toString()), + + /** + * boolean value {@code true} {@code false}. + */ + AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED("authentication.gatewaytoken.enabled", + "hawkbit.server.controller.security.authentication.gatewaytoken.enabled", Boolean.FALSE.toString()), + /** + * string value which holds the name of the security token key. + */ + AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_NAME("authentication.gatewaytoken.name", + "hawkbit.server.controller.security.authentication.gatewaytoken.name", null), + + /** + * string value which holds the actual security-key of the gateway security + * token. + */ + AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY("authentication.gatewaytoken.key", + "hawkbit.server.controller.security.authentication.gatewaytoken.key", null); + + private final String keyName; + private final String defaultKeyName; + private final String defaultValue; + + /** + * @param key + * the property key name + * @param allowedValues + * the allowed values for this specific key + */ + private TenantConfigurationKey(final String key, final String defaultKeyName, final String defaultValue) { + this.keyName = key; + this.defaultKeyName = defaultKeyName; + this.defaultValue = defaultValue; + } + + /** + * @return the keyName + */ + public String getKeyName() { + return keyName; + } + + /** + * @return the defaultKeyName + */ + public String getDefaultKeyName() { + return defaultKeyName; + } + + /** + * @return the defaultValue + */ + public String getDefaultValue() { + return defaultValue; + } +} diff --git a/hawkbit-core/src/test/java/org/eclipse/hawkbit/eventbus/EventBusSubscriberProcessorTest.java b/hawkbit-core/src/test/java/org/eclipse/hawkbit/eventbus/EventBusSubscriberProcessorTest.java new file mode 100755 index 000000000..ac02aaad1 --- /dev/null +++ b/hawkbit-core/src/test/java/org/eclipse/hawkbit/eventbus/EventBusSubscriberProcessorTest.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus; + +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; + +@RunWith(MockitoJUnitRunner.class) +public class EventBusSubscriberProcessorTest { + + @Mock + private EventBus eventBusMock; + + private final EventBusSubscriberProcessor postProcessorUnderTest = new EventBusSubscriberProcessor(); + + @Before + public void before() { + reset(eventBusMock); + postProcessorUnderTest.setEventBus(eventBusMock); + } + + @Test + public void correctAnnotatedClassAndMethodIsRegistered() { + final TestEventSubscriberClass testEventSubscriberClass = new TestEventSubscriberClass(); + postProcessorUnderTest.postProcessAfterInitialization(testEventSubscriberClass, "correctEventSubscriber"); + verify(eventBusMock, times(1)).register(testEventSubscriberClass); + } + + @Test + public void eventSubscriberWithoutMethodAnnotationIsNotRegistered() { + final TestWrongEventSubscriberClass testEventSubscriberClass = new TestWrongEventSubscriberClass(); + postProcessorUnderTest.postProcessAfterInitialization(testEventSubscriberClass, "correctEventSubscriber"); + verify(eventBusMock, times(0)).register(testEventSubscriberClass); + } + + @EventSubscriber + private class TestEventSubscriberClass { + @Subscribe + public void subscribe(final String s) { + + } + } + + @EventSubscriber + private class TestWrongEventSubscriberClass { + public void methodWithoutAnnotation(final String s) { + + } + } +} diff --git a/hawkbit-dmf-amqp/pom.xml b/hawkbit-dmf-amqp/pom.xml new file mode 100755 index 000000000..266412e8e --- /dev/null +++ b/hawkbit-dmf-amqp/pom.xml @@ -0,0 +1,139 @@ + + + 4.0.0 + + org.eclipse.hawkbit + hawkbit-parent + 0.1.0-SNAPSHOT + + hawkbit-dmf-amqp + hawkBit :: DMF Amqp + + + + + org.eclipse.hawkbit + hawkbit-repository + ${project.version} + + + org.eclipse.hawkbit + hawkbit-security-integration + ${project.version} + + + org.eclipse.hawkbit + hawkbit-dmf-api + ${project.version} + + + org.springframework.amqp + spring-rabbit + + + org.springframework.security + spring-security-web + + + org.springframework.boot + spring-boot + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + org.slf4j + slf4j-api + + + + + + com.h2database + h2 + test + + + org.mariadb.jdbc + mariadb-java-client + test + + + javax.el + javax.el-api + test + + + org.eclipse.hawkbit + hawkbit-repository + ${project.version} + tests + + + ru.yandex.qatools.allure + allure-junit-adaptor + test + + + org.springframework.data + spring-data-rest-webmvc + test + + + org.springframework.security + spring-security-aspects + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework + spring-test + test + + + org.easytesting + fest-assert-core + test + + + org.easytesting + fest-assert + test + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + test + + + org.springframework.security + spring-security-config + test + + + org.springframework + spring-context-support + test + + + + \ No newline at end of file diff --git a/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java new file mode 100755 index 000000000..30cec2f74 --- /dev/null +++ b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java @@ -0,0 +1,151 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.amqp; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.hawkbit.dmf.amqp.api.AmqpSettings; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.FanoutExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * The spring AMQP configuration which is enabled by using the profile + * {@code amqp} to use a AMQP for communication with SP enabled devices. + * + * + * + */ +@EnableConfigurationProperties(AmqpProperties.class) +public class AmqpConfiguration { + + @Autowired + protected AmqpProperties amqpProperties; + + @Autowired + private ConnectionFactory connectionFactory; + + @Autowired + private RabbitTemplate rabbitTemplate; + + /** + * Method to set the Jackson2JsonMessageConverter. + * + * @return the Jackson2JsonMessageConverter + */ + @Bean + public MessageConverter jsonMessageConverter() { + final Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter(); + rabbitTemplate.setMessageConverter(jackson2JsonMessageConverter); + return jackson2JsonMessageConverter; + } + + /** + * Create the sp receiver queue. + * + * @return the receiver queue + */ + @Bean + public Queue receiverQueue() { + return new Queue(amqpProperties.getReceiverQueue(), true, false, false, getDeadLetterExchangeArgs()); + } + + /** + * Create the dead letter fanout exchange. + * + * @return the fanout exchange + */ + @Bean + public FanoutExchange senderExchange() { + return new FanoutExchange(AmqpSettings.DMF_EXCHANGE); + } + + /** + * Create dead letter queue. + * + * @return the queue + */ + @Bean + public Queue deadLetterQueue() { + return new Queue(amqpProperties.getDeadLetterQueue()); + } + + /** + * Create the dead letter fanout exchange. + * + * @return the fanout exchange + */ + @Bean + public FanoutExchange exchangeDeadLetter() { + return new FanoutExchange(amqpProperties.getDeadLetterExchange()); + } + + /** + * Create the Binding deadLetterQueue to exchangeDeadLetter. + * + * @return the binding + */ + @Bean + public Binding bindDeadLetterQueueToLwm2mExchange() { + return BindingBuilder.bind(deadLetterQueue()).to(exchangeDeadLetter()); + } + + /** + * Create the Binding {@link AmqpConfiguration#receiverQueueFromSp()} to + * {@link AmqpConfiguration#senderConnectorToSpExchange()}. + * + * @return the binding and create the queue and exchange + */ + @Bean + public Binding bindSenderExchangeToSpQueue() { + return BindingBuilder.bind(receiverQueue()).to(senderExchange()); + } + + /** + * Create amqp handler service bean. + * + * @return + */ + @Bean + public AmqpMessageHandlerService amqpMessageHandlerService() { + return new AmqpMessageHandlerService(); + } + + /** + * Returns the Listener factory. + * + * @return the {@link SimpleMessageListenerContainer} that gets used receive + * AMQP messages + */ + @Bean(name = { "listenerContainerFactory" }) + public SimpleRabbitListenerContainerFactory listenerContainerFactory() { + final SimpleRabbitListenerContainerFactory containerFactory = new SimpleRabbitListenerContainerFactory(); + containerFactory.setDefaultRequeueRejected(false); + containerFactory.setConnectionFactory(connectionFactory); + return containerFactory; + } + + private Map getDeadLetterExchangeArgs() { + final Map args = new HashMap(); + args.put("x-dead-letter-exchange", amqpProperties.getDeadLetterExchange()); + return args; + } + +} diff --git a/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthentfication.java b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthentfication.java new file mode 100755 index 000000000..ee31470f6 --- /dev/null +++ b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthentfication.java @@ -0,0 +1,152 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.amqp; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.PostConstruct; + +import org.eclipse.hawkbit.dmf.json.model.TenantSecruityToken; +import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails; +import org.eclipse.hawkbit.repository.ControllerManagement; +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.security.CoapAnonymousPreAuthenticatedFilter; +import org.eclipse.hawkbit.security.ControllerPreAuthenticateSecurityTokenFilter; +import org.eclipse.hawkbit.security.ControllerPreAuthenticatedGatewaySecurityTokenFilter; +import org.eclipse.hawkbit.security.ControllerPreAuthenticatedSecurityHeaderFilter; +import org.eclipse.hawkbit.security.PreAuthTokenSourceTrustAuthenticationProvider; +import org.eclipse.hawkbit.security.PreAuthenficationFilter; +import org.eclipse.hawkbit.security.SecurityProperties; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.stereotype.Component; + +/** + * + * + */ +@Component +public class AmqpControllerAuthentfication { + + private static final Logger LOGGER = LoggerFactory.getLogger(AmqpControllerAuthentfication.class); + + private final PreAuthTokenSourceTrustAuthenticationProvider preAuthenticatedAuthenticationProvider; + + private final List filterChain = new ArrayList<>(); + + @Autowired + private ControllerManagement controllerManagement; + + @Autowired + private SystemManagement systemManagement; + + @Autowired + private TenantAware tenantAware; + + @Autowired + private SecurityProperties secruityProperties; + + /** + * Constructor. + */ + public AmqpControllerAuthentfication() { + preAuthenticatedAuthenticationProvider = new PreAuthTokenSourceTrustAuthenticationProvider(); + } + + /** + * Called by spring when bean instantiated and autowired. + */ + @PostConstruct + public void postConstruct() { + addFilter(); + } + + private void addFilter() { + final ControllerPreAuthenticatedGatewaySecurityTokenFilter gatewaySecurityTokenFilter = new ControllerPreAuthenticatedGatewaySecurityTokenFilter( + systemManagement, tenantAware); + filterChain.add(gatewaySecurityTokenFilter); + + final ControllerPreAuthenticatedSecurityHeaderFilter securityHeaderFilter = new ControllerPreAuthenticatedSecurityHeaderFilter( + secruityProperties.getRpCnHeader(), secruityProperties.getRpSslIssuerHashHeader(), systemManagement, + tenantAware); + filterChain.add(securityHeaderFilter); + + final ControllerPreAuthenticateSecurityTokenFilter securityTokenFilter = new ControllerPreAuthenticateSecurityTokenFilter( + systemManagement, controllerManagement, tenantAware); + filterChain.add(securityTokenFilter); + + filterChain.add(new CoapAnonymousPreAuthenticatedFilter()); + } + + /** + * Performs authentication with the secruity token. + * + * @param secruityToken + * the authentication request object + * @return the authentfication object + */ + public Authentication doAuthenticate(final TenantSecruityToken secruityToken) { + PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(null, null); + for (final PreAuthenficationFilter filter : filterChain) { + final PreAuthenticatedAuthenticationToken authenticationRest = createAuthentication(filter, secruityToken); + if (authenticationRest != null) { + authentication = authenticationRest; + authentication.setDetails(new TenantAwareAuthenticationDetails(secruityToken.getTenant(), true)); + break; + } + } + return preAuthenticatedAuthenticationProvider.authenticate(authentication); + + } + + private PreAuthenticatedAuthenticationToken createAuthentication(final PreAuthenficationFilter filter, + final TenantSecruityToken secruityToken) { + + if (!filter.isEnable(secruityToken)) { + return null; + } + + final Object principal = filter.getPreAuthenticatedPrincipal(secruityToken); + final Object credentials = filter.getPreAuthenticatedCredentials(secruityToken); + + if (principal == null) { + LOGGER.debug("No pre-authenticated principal found in message"); + return null; + } + + LOGGER.debug("preAuthenticatedPrincipal = {} trying to authenticate", principal); + + final PreAuthenticatedAuthenticationToken authRequest = new PreAuthenticatedAuthenticationToken(principal, + credentials); + + return authRequest; + } + + public void setControllerManagement(final ControllerManagement controllerManagement) { + this.controllerManagement = controllerManagement; + } + + public void setSecruityProperties(final SecurityProperties secruityProperties) { + this.secruityProperties = secruityProperties; + } + + public void setSystemManagement(final SystemManagement systemManagement) { + this.systemManagement = systemManagement; + } + + public void setTenantAware(final TenantAware tenantAware) { + this.tenantAware = tenantAware; + } + +} diff --git a/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java new file mode 100755 index 000000000..933124560 --- /dev/null +++ b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java @@ -0,0 +1,182 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.amqp; + +import java.net.URI; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.dmf.amqp.api.EventTopic; +import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey; +import org.eclipse.hawkbit.dmf.amqp.api.MessageType; +import org.eclipse.hawkbit.dmf.json.model.Artifact; +import org.eclipse.hawkbit.dmf.json.model.ArtifactHash; +import org.eclipse.hawkbit.dmf.json.model.DownloadAndUpdateRequest; +import org.eclipse.hawkbit.dmf.json.model.SoftwareModule; +import org.eclipse.hawkbit.eventbus.EventSubscriber; +import org.eclipse.hawkbit.eventbus.event.CancelTargetAssignmentEvent; +import org.eclipse.hawkbit.eventbus.event.TargetAssignDistributionSetEvent; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.eclipse.hawkbit.util.ArtifactUrlHandler; +import org.eclipse.hawkbit.util.IpUtil; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; + +import com.google.common.eventbus.Subscribe; + +/** + * {@link AmqpMessageDispatcherService} handles all outgoing AMQP messages. + * + * + * + */ +@EventSubscriber +public class AmqpMessageDispatcherService { + + @Autowired + private RabbitTemplate rabbitTemplate; + + @Autowired + private TenantAware tenantAware; + + @Autowired + private ArtifactUrlHandler artifactUrlHandler; + + /** + * Method to send a message to a RabbitMQ Exchange after the Distribution + * set has been assign to a Target. + * + * @param targetAssignDistributionSetEvent + * the object to be send. + */ + @Subscribe + public void targetAssignDistributionSet(final TargetAssignDistributionSetEvent targetAssignDistributionSetEvent) { + final URI targetAdress = targetAssignDistributionSetEvent.getTargetAdress(); + if (!IpUtil.isAmqpUri(targetAdress)) { + return; + } + + final String controllerId = targetAssignDistributionSetEvent.getControllerId(); + final Collection modules = targetAssignDistributionSetEvent + .getSoftwareModules(); + final DownloadAndUpdateRequest downloadAndUpdateRequest = new DownloadAndUpdateRequest(); + downloadAndUpdateRequest.setActionId(targetAssignDistributionSetEvent.getActionId()); + + for (final org.eclipse.hawkbit.repository.model.SoftwareModule softwareModule : modules) { + final SoftwareModule amqpSoftwareModule = convertToAmqpSoftwareModule(controllerId, softwareModule); + downloadAndUpdateRequest.addSoftwareModule(amqpSoftwareModule); + } + + final Message message = rabbitTemplate.getMessageConverter().toMessage(downloadAndUpdateRequest, + createConnectorMessageProperties(controllerId, EventTopic.DOWNLOAD_AND_INSTALL)); + sendMessage(targetAdress.getHost(), message); + } + + /** + * Method to send a message to a RabbitMQ Exchange after the assignment of + * the Distribution set to a Target has been canceled. + * + * @param cancelTargetAssignmentDistributionSetEvent + * the object to be send. + */ + @Subscribe + public void targetCancelAssignmentToDistributionSet( + final CancelTargetAssignmentEvent cancelTargetAssignmentDistributionSetEvent) { + final String controllerId = cancelTargetAssignmentDistributionSetEvent.getControllerId(); + final Long actionId = cancelTargetAssignmentDistributionSetEvent.getActionId(); + final Message message = rabbitTemplate.getMessageConverter().toMessage(actionId, + createConnectorMessageProperties(controllerId, EventTopic.CANCEL_DOWNLOAD)); + + sendMessage(cancelTargetAssignmentDistributionSetEvent.getTargetAdress().getHost(), message); + + } + + /** + * Send message to exchange. + * + * @param exchange + * the exchange + * @param message + * the message + */ + public void sendMessage(final String exchange, final Message message) { + rabbitTemplate.setExchange(exchange); + rabbitTemplate.send(message); + } + + private MessageProperties createConnectorMessageProperties(final String controllerId, final EventTopic topic) { + final MessageProperties messageProperties = createMessageProperties(); + messageProperties.setHeader(MessageHeaderKey.TOPIC, topic); + messageProperties.setHeader(MessageHeaderKey.THING_ID, controllerId); + messageProperties.setHeader(MessageHeaderKey.TENANT, tenantAware.getCurrentTenant()); + messageProperties.setHeader(MessageHeaderKey.TYPE, MessageType.EVENT); + return messageProperties; + } + + private MessageProperties createMessageProperties() { + final MessageProperties messageProperties = new MessageProperties(); + messageProperties.setContentType(MessageProperties.CONTENT_TYPE_JSON); + messageProperties.setHeader(MessageHeaderKey.CONTENT_TYPE, MessageProperties.CONTENT_TYPE_JSON); + return messageProperties; + } + + private SoftwareModule convertToAmqpSoftwareModule(final String targetId, + final org.eclipse.hawkbit.repository.model.SoftwareModule softwareModule) { + final SoftwareModule amqpSoftwareModule = new SoftwareModule(); + amqpSoftwareModule.setModuleId(softwareModule.getId()); + amqpSoftwareModule.setModuleType(softwareModule.getType().getKey()); + amqpSoftwareModule.setModuleVersion(softwareModule.getVersion()); + + final List artifacts = convertArtifacts(targetId, softwareModule.getLocalArtifacts()); + amqpSoftwareModule.setArtifacts(artifacts); + return amqpSoftwareModule; + } + + private List convertArtifacts(final String targetId, final List localArtifacts) { + if (localArtifacts.isEmpty()) { + return Collections.emptyList(); + } + + final List convertedArtifacts = localArtifacts.stream() + .map(localArtifact -> convertArtifact(targetId, localArtifact)).collect(Collectors.toList()); + return convertedArtifacts; + } + + private Artifact convertArtifact(final String targetId, final LocalArtifact localArtifact) { + final Artifact artifact = new Artifact(); + artifact.getUrls().put(Artifact.UrlProtocol.COAP, + artifactUrlHandler.getUrl(targetId, localArtifact, Artifact.UrlProtocol.COAP)); + artifact.getUrls().put(Artifact.UrlProtocol.HTTP, + artifactUrlHandler.getUrl(targetId, localArtifact, Artifact.UrlProtocol.HTTP)); + artifact.getUrls().put(Artifact.UrlProtocol.HTTPS, + artifactUrlHandler.getUrl(targetId, localArtifact, Artifact.UrlProtocol.HTTPS)); + + artifact.setFilename(localArtifact.getFilename()); + artifact.setHashes(new ArtifactHash(localArtifact.getSha1Hash(), null)); + artifact.setSize(localArtifact.getSize()); + return artifact; + } + + public void setTenantAware(final TenantAware tenantAware) { + this.tenantAware = tenantAware; + } + + public void setRabbitTemplate(final RabbitTemplate rabbitTemplate) { + this.rabbitTemplate = rabbitTemplate; + } + + public void setArtifactUrlHandler(final ArtifactUrlHandler artifactUrlHandler) { + this.artifactUrlHandler = artifactUrlHandler; + } +} diff --git a/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java new file mode 100755 index 000000000..33a08980b --- /dev/null +++ b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java @@ -0,0 +1,417 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.amqp; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.hawkbit.api.HostnameResolver; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash; +import org.eclipse.hawkbit.cache.CacheConstants; +import org.eclipse.hawkbit.cache.DownloadArtifactCache; +import org.eclipse.hawkbit.cache.DownloadType; +import org.eclipse.hawkbit.dmf.amqp.api.EventTopic; +import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey; +import org.eclipse.hawkbit.dmf.amqp.api.MessageType; +import org.eclipse.hawkbit.dmf.json.model.ActionUpdateStatus; +import org.eclipse.hawkbit.dmf.json.model.Artifact; +import org.eclipse.hawkbit.dmf.json.model.ArtifactHash; +import org.eclipse.hawkbit.dmf.json.model.DownloadResponse; +import org.eclipse.hawkbit.dmf.json.model.TenantSecruityToken; +import org.eclipse.hawkbit.eventbus.event.TargetAssignDistributionSetEvent; +import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails; +import org.eclipse.hawkbit.repository.ArtifactManagement; +import org.eclipse.hawkbit.repository.ControllerManagement; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.util.IpUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.AbstractJavaTypeMapper; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.cache.Cache; +import org.springframework.http.HttpStatus; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.CredentialsExpiredException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.web.util.UriComponentsBuilder; + +import com.google.common.eventbus.EventBus; + +/** + * + * {@link AmqpMessageHandlerService} handles all incoming AMQP messages. + * + * + * + * + */ +public class AmqpMessageHandlerService { + + private static final Logger LOG = LoggerFactory.getLogger(AmqpMessageHandlerService.class); + + @Autowired + private RabbitTemplate rabbitTemplate; + + @Autowired + private ControllerManagement controllerManagement; + + @Autowired + private AmqpControllerAuthentfication authenticationManager; + + @Autowired + private ArtifactManagement artifactManagement; + + @Autowired + private EventBus eventBus; + + @Autowired + @Qualifier(CacheConstants.DOWNLOAD_ID_CACHE) + private Cache cache; + + @Autowired + private HostnameResolver hostnameResolver; + + /** + * /** Method to handle all incoming amqp messages. + * + * @param message + * incoming message + * @param type + * the message type + * @param contentType + * the contentType of the message + * @param tenant + * the contentType of the message + * @return a message if no message is send back to sender + */ + @RabbitListener(queues = "${hawkbit.dmf.rabbitmq.receiverQueue}", containerFactory = "listenerContainerFactory") + public Message onMessage(final Message message, @Header(MessageHeaderKey.TYPE) final String type, + @Header(MessageHeaderKey.TENANT) final String tenant) { + checkContentTypeJson(message); + final SecurityContext oldContext = SecurityContextHolder.getContext(); + try { + final MessageType messageType = MessageType.valueOf(type); + switch (messageType) { + case THING_CREATED: + setTenantSecurityContext(tenant); + registerTarget(message); + break; + case EVENT: + setTenantSecurityContext(tenant); + final String topicValue = getStringHeaderKey(message, MessageHeaderKey.TOPIC, "EventTopic is null"); + final EventTopic eventTopic = EventTopic.valueOf(topicValue); + handleIncomingEvent(message, eventTopic); + break; + case AUTHENTIFICATION: + return handleAuthentifiactionMessage(message); + default: + logAndThrowMessageError(message, "No handle method was found for the given message type."); + } + } finally { + SecurityContextHolder.setContext(oldContext); + } + return null; + } + + private Message handleAuthentifiactionMessage(final Message message) { + final DownloadResponse authentificationResponse = new DownloadResponse(); + final MessageProperties messageProperties = message.getMessageProperties(); + final TenantSecruityToken secruityToken = convertMessage(message, TenantSecruityToken.class); + final String sha1 = secruityToken.getSha1(); + try { + SecurityContextHolder.getContext().setAuthentication(authenticationManager.doAuthenticate(secruityToken)); + final LocalArtifact localArtifact = artifactManagement + .findFirstLocalArtifactsBySHA1(secruityToken.getSha1()); + if (localArtifact == null) { + throw new EntityNotFoundException(); + } + + // check action for this download purposes, the method will throw an + // EntityNotFoundException in case the controller is not allowed to + // download this file + // because it's not assigned to an action and not assigned to this + // controller. + final Action action = controllerManagement.getActionForDownloadByTargetAndSoftwareModule( + secruityToken.getControllerId(), localArtifact.getSoftwareModule()); + LOG.info("Found action for download authentication request action: {}, sha1: {}", action, + secruityToken.getSha1()); + + final Artifact artifact = convertDbArtifact(artifactManagement.loadLocalArtifactBinary(localArtifact)); + if (artifact == null) { + throw new EntityNotFoundException(); + } + authentificationResponse.setArtifact(artifact); + final String downloadId = UUID.randomUUID().toString(); + final DownloadArtifactCache downloadCache = new DownloadArtifactCache(DownloadType.BY_SHA1, sha1); + cache.put(downloadId, downloadCache); + authentificationResponse + .setDownloadUrl(UriComponentsBuilder.fromUri(hostnameResolver.resolveHostname().toURI()) + .path("/api/v1/downloadserver/downloadId/").path(downloadId).build().toUriString()); + authentificationResponse.setResponseCode(HttpStatus.OK.value()); + } catch (final BadCredentialsException | AuthenticationServiceException | CredentialsExpiredException e) { + LOG.error("Login failed", e); + authentificationResponse.setResponseCode(HttpStatus.FORBIDDEN.value()); + authentificationResponse.setMessage("Login failed"); + } catch (final URISyntaxException e) { + LOG.error("URI build exception", e); + authentificationResponse.setResponseCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); + authentificationResponse.setMessage("Building download URI failed"); + } catch (final EntityNotFoundException e) { + final String errorMessage = "Artifact with sha1 " + sha1 + "not found "; + LOG.warn(errorMessage, e); + authentificationResponse.setResponseCode(HttpStatus.NOT_FOUND.value()); + authentificationResponse.setMessage(errorMessage); + } + + return rabbitTemplate.getMessageConverter().toMessage(authentificationResponse, messageProperties); + } + + private Artifact convertDbArtifact(final DbArtifact dbArtifact) { + final Artifact artifact = new Artifact(); + artifact.setSize(dbArtifact.getSize()); + final DbArtifactHash dbArtifactHash = dbArtifact.getHashes(); + artifact.setHashes(new ArtifactHash(dbArtifactHash.getSha1(), dbArtifactHash.getMd5())); + return artifact; + } + + protected void logAndThrowMessageError(final Message message, final String error) { + LOG.error("Error \"{}\" reported by message {}", error, message.getMessageProperties().getMessageId()); + throw new IllegalArgumentException(error); + } + + private void setSecurityContext(final Authentication authentication) { + final SecurityContextImpl securityContextImpl = new SecurityContextImpl(); + securityContextImpl.setAuthentication(authentication); + SecurityContextHolder.setContext(securityContextImpl); + } + + private void setTenantSecurityContext(final String tenantId) { + final AnonymousAuthenticationToken authenticationToken = new AnonymousAuthenticationToken( + UUID.randomUUID().toString(), "AMQP-Controller", + Collections.singletonList(new SimpleGrantedAuthority(SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS))); + authenticationToken.setDetails(new TenantAwareAuthenticationDetails(tenantId, true)); + setSecurityContext(authenticationToken); + } + + private String getStringHeaderKey(final Message message, final String key, final String errorMessageIfNull) { + final Map header = message.getMessageProperties().getHeaders(); + final Object value = header.get(key); + if (value == null) { + logAndThrowMessageError(message, errorMessageIfNull); + } + return value.toString(); + } + + /** + * Method to create a new target or to find the target if it already exists. + * + * @param targetID + * the ID of the target/thing + * @param ip + * the ip of the target/thing + */ + private void registerTarget(final Message message) { + final String thingId = getStringHeaderKey(message, MessageHeaderKey.THING_ID, "ThingId is null"); + final String replyTo = message.getMessageProperties().getReplyTo(); + + if (StringUtils.isEmpty(replyTo)) { + logAndThrowMessageError(message, "No ReplyTo was set for the createThing Event."); + } + final URI amqpUri = IpUtil.createAmqpUri(replyTo); + final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotexist(thingId, amqpUri); + LOG.debug("Target {} reported online state.", thingId); + + lookIfUpdateAvailable(target); + } + + private void lookIfUpdateAvailable(final Target target) { + final List actions = controllerManagement.findActionByTargetAndActive(target); + if (actions.isEmpty()) { + return; + } + // action are ordered by ASC + final Action action = actions.get(0); + final DistributionSet distributionSet = action.getDistributionSet(); + final List softwareModuleList = controllerManagement + .findSoftwareModulesByDistributionSet(distributionSet); + eventBus.post(new TargetAssignDistributionSetEvent(target.getControllerId(), action.getId(), softwareModuleList, + target.getTargetInfo().getAddress())); + + } + + /** + * Method to handle the different topics to an event. + * + * @param message + * the incoming event message. + * @param topic + * the topic of the event. + */ + private void handleIncomingEvent(final Message message, final EventTopic topic) { + switch (topic) { + case UPDATE_ACTION_STATUS: + updateActionStatus(message); + return; + default: + logAndThrowMessageError(message, "Got event without appropriate topic."); + } + } + + /** + * Method to update the action status of an action through the event. + * + * @param actionUpdateStatus + * the object form the ampq message + */ + private void updateActionStatus(final Message message) { + final ActionUpdateStatus actionUpdateStatus = convertMessage(message, ActionUpdateStatus.class); + final Long actionId = actionUpdateStatus.getActionId(); + LOG.debug("Target notifies intermediate about action {} with status {}.", actionId, + actionUpdateStatus.getActionStatus().name()); + + if (actionId == null) { + logAndThrowMessageError(message, "Invalid message no action id"); + } + + final Action action = controllerManagement.findActionWithDetails(actionId); + + if (action == null) { + logAndThrowMessageError(message, + "Got intermediate notification about action " + actionId + " but action does not exist"); + } + + final ActionStatus actionStatus = new ActionStatus(); + final List messageText = actionUpdateStatus.getMessage(); + final String messageString = String.join(", ", messageText); + actionStatus.addMessage(messageString); + actionStatus.setAction(action); + actionStatus.setOccurredAt(System.currentTimeMillis()); + + switch (actionUpdateStatus.getActionStatus()) { + case DOWNLOAD: + actionStatus.setStatus(Status.DOWNLOAD); + break; + case RETRIEVED: + actionStatus.setStatus(Status.RETRIEVED); + break; + case RUNNING: + actionStatus.setStatus(Status.RUNNING); + break; + case FINISHED: + actionStatus.setStatus(Status.FINISHED); + break; + case ERROR: + actionStatus.setStatus(Status.ERROR); + break; + case WARNING: + actionStatus.setStatus(Status.WARNING); + break; + default: + logAndThrowMessageError(message, "Status for action does not exisit."); + } + + final Action savedAction = controllerManagement.addUpdateActionStatus(actionStatus, action); + if (Status.FINISHED == savedAction.getStatus()) { + lookIfUpdateAvailable(action.getTarget()); + } + } + + /** + * Is needed to convert a incoming message to is originally object type. + * + * @param message + * the message to convert. + * @param clazz + * the class of the originally object. + * @return + */ + @SuppressWarnings("unchecked") + private T convertMessage(final Message message, final Class clazz) { + message.getMessageProperties().getHeaders().put(AbstractJavaTypeMapper.DEFAULT_CLASSID_FIELD_NAME, + clazz.getTypeName()); + return (T) rabbitTemplate.getMessageConverter().fromMessage(message); + } + + /** + * Is needed to verify if an incoming message has the content type json. + * + * @param message + * the to verify + * @param contentType + * the content type + * @return true if the content type has json, false it not. + */ + private void checkContentTypeJson(final Message message) { + final MessageProperties messageProperties = message.getMessageProperties(); + if (messageProperties.getContentType() != null && messageProperties.getContentType().contains("json")) { + return; + } + throw new IllegalArgumentException("Content-Type is not JSON compatible"); + } + + void setControllerManagement(final ControllerManagement controllerManagement) { + this.controllerManagement = controllerManagement; + } + + void setHostnameResolver(final HostnameResolver hostnameResolver) { + this.hostnameResolver = hostnameResolver; + } + + void setRabbitTemplate(final RabbitTemplate rabbitTemplate) { + this.rabbitTemplate = rabbitTemplate; + } + + MessageConverter getMessageConverter() { + return rabbitTemplate.getMessageConverter(); + } + + void setAuthenticationManager(final AmqpControllerAuthentfication authenticationManager) { + this.authenticationManager = authenticationManager; + } + + void setArtifactManagement(final ArtifactManagement artifactManagement) { + this.artifactManagement = artifactManagement; + } + + void setCache(final Cache cache) { + this.cache = cache; + } + + void setEventBus(final EventBus eventBus) { + this.eventBus = eventBus; + } + +} diff --git a/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpProperties.java b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpProperties.java new file mode 100755 index 000000000..338f514ad --- /dev/null +++ b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpProperties.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.amqp; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Bean which holds the necessary properties for configuring the AMQP + * connection. + * + * + * + * + */ +@ConfigurationProperties("hawkbit.dmf.rabbitmq") +public class AmqpProperties { + + private String deadLetterQueue = "dmf_connector_deadletter"; + private String deadLetterExchange = "dmf.connector.deadletter"; + private String receiverQueue = "dmf_receiver"; + + /** + * Returns the dead letter exchange. + * + * @return dead letter exchange + */ + public String getDeadLetterExchange() { + return deadLetterExchange; + } + + /** + * Sets the dead letter exchange. + * + * @param deadLetterExchange + * the deadLetterExchange to be set + */ + public void setDeadLetterExchange(final String deadLetterExchange) { + this.deadLetterExchange = deadLetterExchange; + } + + /** + * Returns the dead letter queue. + * + * @return the dead letter queue + */ + public String getDeadLetterQueue() { + return deadLetterQueue; + } + + /** + * Sets the dead letter queue. + * + * @param deadLetterQueue + * the deadLetterQueue ro be set + */ + public void setDeadLetterQueue(final String deadLetterQueue) { + this.deadLetterQueue = deadLetterQueue; + } + + public String getReceiverQueue() { + return receiverQueue; + } + + public void setReceiverQueue(final String receiverQueue) { + this.receiverQueue = receiverQueue; + } +} diff --git a/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/annotation/EnableAmqp.java b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/annotation/EnableAmqp.java new file mode 100755 index 000000000..2eb20c60b --- /dev/null +++ b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/annotation/EnableAmqp.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.amqp.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.hawkbit.amqp.AmqpConfiguration; +import org.springframework.context.annotation.Import; + +/** + * Annotation to enable amqp. + * + * + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(AmqpConfiguration.class) +public @interface EnableAmqp { + +} diff --git a/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/util/ArtifactUrlHandler.java b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/util/ArtifactUrlHandler.java new file mode 100755 index 000000000..488cfa2cb --- /dev/null +++ b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/util/ArtifactUrlHandler.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.util; + +import org.eclipse.hawkbit.dmf.json.model.Artifact; +import org.eclipse.hawkbit.repository.model.LocalArtifact; + +/** + * Interface declaration of the {@link ArtifactUrlHandler} which generates the + * URLs to specific artifacts. + * + * + * + */ +public interface ArtifactUrlHandler { + + /** + * Returns a generated URL for a given artifact for a specific protocol. + * + * @param controllerId + * the authentifacted controller id + * @param localArtifact + * the artifact to retrieve a URL to + * @param protocol + * the protocol the URL should be generated + * @return an URL for the given artifact in a given protocol + */ + String getUrl(String controllerId, LocalArtifact localArtifact, final Artifact.UrlProtocol protocol); + +} diff --git a/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/util/ArtifactUrlHandlerProperties.java b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/util/ArtifactUrlHandlerProperties.java new file mode 100755 index 000000000..91c385fae --- /dev/null +++ b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/util/ArtifactUrlHandlerProperties.java @@ -0,0 +1,337 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.util; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * + * + */ +@ConfigurationProperties("hawkbit.artifact.url") +public class ArtifactUrlHandlerProperties { + private static final String DEFAULT_IP_LOCALHOST = "127.0.0.1"; + private static final String LOCALHOST = "localhost"; + + private final Http http = new Http(); + private final Https https = new Https(); + private final Coap coap = new Coap(); + + /** + * @return the http + */ + public Http getHttp() { + return http; + } + + /** + * @return the https + */ + public Https getHttps() { + return https; + } + + /** + * @return the coap + */ + public Coap getCoap() { + return coap; + } + + /** + * @param protocol + * the protocol schema to retrieve the properties. + * @return the properties to a protocol or {@code null} if protocol does not + * have properties or protocol not supported + */ + public ProtocolProperties getProperties(final String protocol) { + switch (protocol) { + case "http": + return getHttp(); + case "https": + return getHttps(); + case "coap": + return getCoap(); + default: + return null; + } + } + + /** + * Interface for declaring common properties through all supported protocols + * pattern. + * + * + * + */ + public interface ProtocolProperties { + /** + * @return the hostname value to resolve in the pattern. + */ + String getHostname(); + + /** + * @return the IP address value to resolve in the pattern. + */ + String getIp(); + + /** + * @return the port value to resolve in the pattern. + */ + String getPort(); + + /** + * @return the pattern to build the URL. + */ + String getPattern(); + } + + /** + * Object to hold the properties for the HTTP protocol. + * + * + * + */ + public static class Http implements ProtocolProperties { + private String hostname = LOCALHOST; + private String ip = DEFAULT_IP_LOCALHOST; + private String port = ""; + /** + * An ant-URL pattern with placeholder to build the URL on. The URL can + * have specific artifact placeholder. + */ + private String pattern = "{protocol}://{hostname}:{port}/{tenant}/controller/v1/{targetId}/softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}"; + + /** + * @return the hostname + */ + @Override + public String getHostname() { + return hostname; + } + + /** + * @param hostname + * the hostname to set + */ + public void setHostname(final String hostname) { + this.hostname = hostname; + } + + /** + * @return the ip + */ + @Override + public String getIp() { + return ip; + } + + /** + * @param ip + * the ip to set + */ + public void setIp(final String ip) { + this.ip = ip; + } + + /** + * @return the urlPattern + */ + @Override + public String getPattern() { + return pattern; + } + + /** + * @param urlPattern + * the urlPattern to set + */ + public void setPattern(final String urlPattern) { + this.pattern = urlPattern; + } + + /** + * @return the port + */ + @Override + public String getPort() { + return port; + } + + /** + * @param port + * the port to set + */ + public void setPort(final String port) { + this.port = port; + } + } + + /** + * Object to hold the properties for the HTTP protocol. + * + * + * + */ + public static class Https implements ProtocolProperties { + private String hostname = LOCALHOST; + private String ip = DEFAULT_IP_LOCALHOST; + private String port = ""; + /** + * An ant-URL pattern with placeholder to build the URL on. The URL can + * have specific artifact placeholder. + */ + private String pattern = "{protocol}://{hostname}:{port}/{tenant}/controller/v1/{targetId}/softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}"; + + /** + * @return the hostname + */ + @Override + public String getHostname() { + return hostname; + } + + /** + * @param hostname + * the hostname to set + */ + public void setHostname(final String hostname) { + this.hostname = hostname; + } + + /** + * @return the ip + */ + @Override + public String getIp() { + return ip; + } + + /** + * @param ip + * the ip to set + */ + public void setIp(final String ip) { + this.ip = ip; + } + + /** + * @return the urlPattern + */ + @Override + public String getPattern() { + return pattern; + } + + /** + * @param urlPattern + * the urlPattern to set + */ + public void setPattern(final String urlPattern) { + this.pattern = urlPattern; + } + + /** + * @return the port + */ + @Override + public String getPort() { + return port; + } + + /** + * @param port + * the port to set + */ + public void setPort(final String port) { + this.port = port; + } + } + + /** + * Object to hold the properties for the HTTP protocol. + * + * + * + */ + public static class Coap implements ProtocolProperties { + private String hostname = LOCALHOST; + private String ip = DEFAULT_IP_LOCALHOST; + private String port = "5683"; + /** + * An ant-URL pattern with placeholder to build the URL on. The URL can + * have specific artifact placeholder. + */ + private String pattern = "{protocol}://{ip}:{port}/fw/{tenant}/{targetId}/sha1/{artifactSHA1}"; + + /** + * @return the hostname + */ + @Override + public String getHostname() { + return hostname; + } + + /** + * @param hostname + * the hostname to set + */ + public void setHostname(final String hostname) { + this.hostname = hostname; + } + + /** + * @return the ip + */ + @Override + public String getIp() { + return ip; + } + + /** + * @param ip + * the ip to set + */ + public void setIp(final String ip) { + this.ip = ip; + } + + /** + * @return the urlPattern + */ + @Override + public String getPattern() { + return pattern; + } + + /** + * @param urlPattern + * the urlPattern to set + */ + public void setPattern(final String urlPattern) { + this.pattern = urlPattern; + } + + /** + * @return the port + */ + @Override + public String getPort() { + return port; + } + + /** + * @param port + * the port to set + */ + public void setPort(final String port) { + this.port = port; + } + } +} diff --git a/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/util/PropertyBasedArtifactUrlHandler.java b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/util/PropertyBasedArtifactUrlHandler.java new file mode 100755 index 000000000..8d4cfbc28 --- /dev/null +++ b/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/util/PropertyBasedArtifactUrlHandler.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.eclipse.hawkbit.dmf.json.model.Artifact; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.eclipse.hawkbit.util.ArtifactUrlHandlerProperties.ProtocolProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Component; + +import com.google.common.base.Strings; + +/** + * + * + */ +@Component +@EnableConfigurationProperties(ArtifactUrlHandlerProperties.class) +public class PropertyBasedArtifactUrlHandler implements ArtifactUrlHandler { + + private static final String PROTOCOL_PLACEHOLDER = "protocol"; + private static final String TARGET_ID_PLACEHOLDER = "targetId"; + private static final String IP_PLACEHOLDER = "ip"; + private static final String PORT_PLACEHOLDER = "port"; + private static final String HOSTNAME_PLACEHOLDER = "hostname"; + private static final String ARTIFACT_FILENAME_PLACEHOLDER = "artifactFileName"; + private static final String ARTIFACT_SHA1_PLACEHOLDER = "artifactSHA1"; + private static final String TENANT_PLACEHOLDER = "tenant"; + private static final String SOFTWARE_MODULE_ID_PLACDEHOLDER = "softwareModuleId"; + + @Autowired + private ArtifactUrlHandlerProperties urlHandlerProperties; + + @Autowired + private TenantAware tenantAware; + + @Override + public String getUrl(final String targetId, final LocalArtifact artifact, final Artifact.UrlProtocol protocol) { + final String protocolString = protocol.name().toLowerCase(); + final ProtocolProperties properties = urlHandlerProperties.getProperties(protocolString); + if (properties == null || properties.getPattern() == null) { + return null; + } + + String urlPattern = properties.getPattern(); + final Set> entrySet = getReplaceMap(targetId, artifact, protocolString, properties) + .entrySet(); + for (final Entry entry : entrySet) { + if (entry.getKey().equals(PORT_PLACEHOLDER)) { + urlPattern = urlPattern.replace(":{" + entry.getKey() + "}", + Strings.isNullOrEmpty(entry.getValue()) ? "" : ":" + entry.getValue()); + } else { + urlPattern = urlPattern.replace("{" + entry.getKey() + "}", entry.getValue()); + } + } + return urlPattern; + } + + private Map getReplaceMap(final String targetId, final LocalArtifact artifact, + final String protocol, final ProtocolProperties properties) { + final Map replaceMap = new HashMap<>(); + replaceMap.put(IP_PLACEHOLDER, properties.getIp()); + replaceMap.put(HOSTNAME_PLACEHOLDER, properties.getHostname()); + replaceMap.put(ARTIFACT_FILENAME_PLACEHOLDER, artifact.getFilename()); + replaceMap.put(ARTIFACT_SHA1_PLACEHOLDER, artifact.getSha1Hash()); + replaceMap.put(PROTOCOL_PLACEHOLDER, protocol); + replaceMap.put(PORT_PLACEHOLDER, properties.getPort()); + replaceMap.put(TENANT_PLACEHOLDER, tenantAware.getCurrentTenant()); + replaceMap.put(TARGET_ID_PLACEHOLDER, targetId); + replaceMap.put(SOFTWARE_MODULE_ID_PLACDEHOLDER, String.valueOf(artifact.getSoftwareModule().getId())); + return replaceMap; + } + +} diff --git a/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthentficationTest.java b/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthentficationTest.java new file mode 100755 index 000000000..5a77c5fce --- /dev/null +++ b/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthentficationTest.java @@ -0,0 +1,202 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.amqp; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey; +import org.eclipse.hawkbit.dmf.amqp.api.MessageType; +import org.eclipse.hawkbit.dmf.json.model.DownloadResponse; +import org.eclipse.hawkbit.dmf.json.model.TenantSecruityToken; +import org.eclipse.hawkbit.repository.ArtifactManagement; +import org.eclipse.hawkbit.repository.ControllerManagement; +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.security.SecurityContextTenantAware; +import org.eclipse.hawkbit.security.SecurityProperties; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationKey; +import org.junit.Before; +import org.junit.Test; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * + * + */ +@Features("AMQP Authenfication Test") +@Stories("Tests the authenfication") +public class AmqpControllerAuthentficationTest { + + private static final String TENANT = "DEFAULT"; + private static String CONTROLLLER_ID = "123"; + private AmqpMessageHandlerService amqpMessageHandlerService; + private MessageConverter messageConverter; + private SystemManagement systemManagement; + private AmqpControllerAuthentfication authenticationManager; + + @Before + public void before() throws Exception { + amqpMessageHandlerService = new AmqpMessageHandlerService(); + messageConverter = new Jackson2JsonMessageConverter(); + final RabbitTemplate rabbitTemplate = new RabbitTemplate(); + rabbitTemplate.setMessageConverter(messageConverter); + amqpMessageHandlerService.setRabbitTemplate(rabbitTemplate); + + authenticationManager = new AmqpControllerAuthentfication(); + authenticationManager.setControllerManagement(mock(ControllerManagement.class)); + final SecurityProperties secruityProperties = mock(SecurityProperties.class); + when(secruityProperties.getRpSslIssuerHashHeader()).thenReturn("X-Ssl-Issuer-Hash-%d"); + authenticationManager.setSecruityProperties(secruityProperties); + systemManagement = mock(SystemManagement.class); + authenticationManager.setSystemManagement(systemManagement); + when(systemManagement.getConfigurationValue(any(), any())).thenReturn(Boolean.FALSE); + + final ControllerManagement controllerManagement = mock(ControllerManagement.class); + when(controllerManagement.getSecurityTokenByControllerId(anyString())).thenReturn(CONTROLLLER_ID); + authenticationManager.setControllerManagement(controllerManagement); + + amqpMessageHandlerService.setArtifactManagement(mock(ArtifactManagement.class)); + + authenticationManager.setTenantAware(new SecurityContextTenantAware()); + authenticationManager.postConstruct(); + amqpMessageHandlerService.setAuthenticationManager(authenticationManager); + } + + @Test(expected = BadCredentialsException.class) + @Description("Tests authentication manager without principal") + public void testAuthenticationeBadCredantialsWithoutPricipal() { + final TenantSecruityToken securityToken = new TenantSecruityToken(TENANT, CONTROLLLER_ID, "12345"); + authenticationManager.doAuthenticate(securityToken); + fail(); + } + + @Test(expected = BadCredentialsException.class) + @Description("Tests authentication manager without wrong credential") + public void testAuthenticationBadCredantialsWithWrongCredential() { + final TenantSecruityToken securityToken = new TenantSecruityToken(TENANT, CONTROLLLER_ID, "12345"); + when(systemManagement.getConfigurationValue( + eq(TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED), any())) + .thenReturn(Boolean.TRUE); + securityToken.getHeaders().put(TenantSecruityToken.AUTHORIZATION_HEADER, "TargetToken 12" + CONTROLLLER_ID); + authenticationManager.doAuthenticate(securityToken); + fail(); + } + + @Test + @Description("Tests authentication successfull") + public void testSuccessfullAuthentication() { + final TenantSecruityToken securityToken = new TenantSecruityToken(TENANT, CONTROLLLER_ID, "12345"); + when(systemManagement.getConfigurationValue( + eq(TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED), any())) + .thenReturn(Boolean.TRUE); + securityToken.getHeaders().put(TenantSecruityToken.AUTHORIZATION_HEADER, "TargetToken " + CONTROLLLER_ID); + final Authentication authentication = authenticationManager.doAuthenticate(securityToken); + assertThat(authentication).isNotNull(); + } + + @Test + @Description("Tests authentication message without principal") + public void testAuthenticationMessageBadCredantialsWithoutPricipal() { + final MessageProperties messageProperties = createMessageProperties(MessageType.AUTHENTIFICATION); + + final TenantSecruityToken securityToken = new TenantSecruityToken(TENANT, CONTROLLLER_ID, "12345"); + final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(securityToken, + messageProperties); + + // test + final Message onMessage = amqpMessageHandlerService.onMessage(message, MessageType.AUTHENTIFICATION.name(), + TENANT); + + // verify + final DownloadResponse downloadResponse = (DownloadResponse) messageConverter.fromMessage(onMessage); + assertThat(downloadResponse).isNotNull(); + assertThat(downloadResponse.getResponseCode()).isEqualTo(HttpStatus.FORBIDDEN.value()); + } + + @Test + @Description("Tests authentication message without wrong credential") + public void testAuthenticationMessageBadCredantialsWithWrongCredential() { + final MessageProperties messageProperties = createMessageProperties(MessageType.AUTHENTIFICATION); + final TenantSecruityToken securityToken = new TenantSecruityToken(TENANT, CONTROLLLER_ID, "12345"); + when(systemManagement.getConfigurationValue( + eq(TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED), any())) + .thenReturn(Boolean.TRUE); + securityToken.getHeaders().put(TenantSecruityToken.AUTHORIZATION_HEADER, "TargetToken 12" + CONTROLLLER_ID); + final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(securityToken, + messageProperties); + + // test + final Message onMessage = amqpMessageHandlerService.onMessage(message, MessageType.AUTHENTIFICATION.name(), + TENANT); + + // verify + final DownloadResponse downloadResponse = (DownloadResponse) messageConverter.fromMessage(onMessage); + assertThat(downloadResponse).isNotNull(); + assertThat(downloadResponse.getResponseCode()).isEqualTo(HttpStatus.FORBIDDEN.value()); + } + + @Test + @Description("Tests authentication message successfull") + public void testSuccessfullMessageAuthentication() { + final MessageProperties messageProperties = createMessageProperties(MessageType.AUTHENTIFICATION); + final TenantSecruityToken securityToken = new TenantSecruityToken(TENANT, CONTROLLLER_ID, "12345"); + when(systemManagement.getConfigurationValue( + eq(TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED), any())) + .thenReturn(Boolean.TRUE); + securityToken.getHeaders().put(TenantSecruityToken.AUTHORIZATION_HEADER, "TargetToken " + CONTROLLLER_ID); + final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(securityToken, + messageProperties); + + // test + final Message onMessage = amqpMessageHandlerService.onMessage(message, MessageType.AUTHENTIFICATION.name(), + TENANT); + + // verify + final DownloadResponse downloadResponse = (DownloadResponse) messageConverter.fromMessage(onMessage); + assertThat(downloadResponse).isNotNull(); + assertThat(downloadResponse.getResponseCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); + assertThat(SecurityContextHolder.getContext()).isNotNull(); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); + assertThat(SecurityContextHolder.getContext().getAuthentication().getClass().getName()) + .isEqualTo(PreAuthenticatedAuthenticationToken.class.getName()); + + } + + private MessageProperties createMessageProperties(final MessageType type) { + return createMessageProperties(type, "MyTest"); + } + + private MessageProperties createMessageProperties(final MessageType type, final String replyTo) { + final MessageProperties messageProperties = new MessageProperties(); + messageProperties.setHeader(MessageHeaderKey.TYPE, type.name()); + messageProperties.setHeader(MessageHeaderKey.TENANT, TENANT); + messageProperties.setContentType(MessageProperties.CONTENT_TYPE_JSON); + messageProperties.setReplyTo(replyTo); + return messageProperties; + } + +} diff --git a/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java b/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java new file mode 100755 index 000000000..f9c6ff64b --- /dev/null +++ b/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java @@ -0,0 +1,200 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.amqp; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.hawkbit.AbstractIntegrationTestWithMongoDB; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; +import org.eclipse.hawkbit.dmf.amqp.api.EventTopic; +import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey; +import org.eclipse.hawkbit.dmf.amqp.api.MessageType; +import org.eclipse.hawkbit.dmf.json.model.DownloadAndUpdateRequest; +import org.eclipse.hawkbit.eventbus.event.CancelTargetAssignmentEvent; +import org.eclipse.hawkbit.eventbus.event.TargetAssignDistributionSetEvent; +import org.eclipse.hawkbit.repository.model.Artifact; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.util.ArtifactUrlHandler; +import org.eclipse.hawkbit.util.IpUtil; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.test.context.ActiveProfiles; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +@ActiveProfiles({ "test" }) +@Features("AMQP Dispatcher Test") +@Stories("Tests send messages") +public class AmqpMessageDispatcherServiceTest extends AbstractIntegrationTestWithMongoDB { + + private AmqpMessageDispatcherService amqpMessageDispatcherService; + + private MessageConverter messageConverter; + + private RabbitTemplate rabbitTemplate; + + private static final String CONTROLLER_ID = "1"; + + @Override + public void before() throws Exception { + super.before(); + amqpMessageDispatcherService = new AmqpMessageDispatcherService(); + amqpMessageDispatcherService = spy(amqpMessageDispatcherService); + messageConverter = new Jackson2JsonMessageConverter(); + + final ArtifactUrlHandler artifactUrlHandlerMock = Mockito.mock(ArtifactUrlHandler.class); + when(artifactUrlHandlerMock.getUrl(anyString(), any(), anyObject())).thenReturn("http://mockurl"); + + this.rabbitTemplate = Mockito.mock(RabbitTemplate.class); + when(rabbitTemplate.getMessageConverter()).thenReturn(messageConverter); + + amqpMessageDispatcherService.setRabbitTemplate(rabbitTemplate); + amqpMessageDispatcherService.setTenantAware(tenantAware); + amqpMessageDispatcherService.setArtifactUrlHandler(artifactUrlHandlerMock); + } + + @Test + @Description("Verfies that download and install event with no software modul works") + public void testSendDownloadRequesWithEmptySoftwareModules() { + final TargetAssignDistributionSetEvent targetAssignDistributionSetEvent = new TargetAssignDistributionSetEvent( + CONTROLLER_ID, 1l, new ArrayList(), IpUtil.createAmqpUri("mytest")); + amqpMessageDispatcherService.targetAssignDistributionSet(targetAssignDistributionSetEvent); + final Message sendMessage = createArgumentCapture(targetAssignDistributionSetEvent.getTargetAdress().getHost()); + final DownloadAndUpdateRequest downloadAndUpdateRequest = assertDownloadAndInstallMessage(sendMessage); + assertTrue(downloadAndUpdateRequest.getSoftwareModules().isEmpty()); + } + + @Test + @Description("Verfies that download and install event with 3 software moduls and no artifacts works") + public void testSendDownloadRequesWithSoftwareModulesAndNoArtifacts() { + final DistributionSet dsA = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + final TargetAssignDistributionSetEvent targetAssignDistributionSetEvent = new TargetAssignDistributionSetEvent( + CONTROLLER_ID, 1l, dsA.getModules(), IpUtil.createAmqpUri("mytest")); + amqpMessageDispatcherService.targetAssignDistributionSet(targetAssignDistributionSetEvent); + final Message sendMessage = createArgumentCapture(targetAssignDistributionSetEvent.getTargetAdress().getHost()); + final DownloadAndUpdateRequest downloadAndUpdateRequest = assertDownloadAndInstallMessage(sendMessage); + assertEquals(3, downloadAndUpdateRequest.getSoftwareModules().size()); + for (final org.eclipse.hawkbit.dmf.json.model.SoftwareModule softwareModule : downloadAndUpdateRequest + .getSoftwareModules()) { + assertTrue(softwareModule.getArtifacts().isEmpty()); + for (final SoftwareModule softwareModule2 : dsA.getModules()) { + assertNotNull(softwareModule.getModuleId()); + if (!softwareModule.getModuleId().equals(softwareModule2.getId())) { + continue; + } + assertEquals(softwareModule.getModuleType(), softwareModule2.getType().getKey()); + assertEquals(softwareModule.getModuleVersion(), softwareModule2.getVersion()); + } + } + } + + @Test + @Description("Verfies that download and install event with software moduls and artifacts works") + public void testSendDownloadRequest() { + final DistributionSet dsA = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + final SoftwareModule module = dsA.getModules().iterator().next(); + final List receivedList = new ArrayList<>(); + for (final Artifact artifact : TestDataUtil.generateArtifacts(artifactManagement, module.getId())) { + module.addArtifact((LocalArtifact) artifact); + receivedList.add(new DbArtifact()); + } + + Mockito.when(rabbitTemplate.convertSendAndReceive(any())).thenReturn(receivedList); + + final TargetAssignDistributionSetEvent targetAssignDistributionSetEvent = new TargetAssignDistributionSetEvent( + CONTROLLER_ID, 1l, dsA.getModules(), IpUtil.createAmqpUri("mytest")); + amqpMessageDispatcherService.targetAssignDistributionSet(targetAssignDistributionSetEvent); + final Message sendMessage = createArgumentCapture(targetAssignDistributionSetEvent.getTargetAdress().getHost()); + final DownloadAndUpdateRequest downloadAndUpdateRequest = assertDownloadAndInstallMessage(sendMessage); + assertEquals(3, downloadAndUpdateRequest.getSoftwareModules().size()); + for (final org.eclipse.hawkbit.dmf.json.model.SoftwareModule softwareModule : downloadAndUpdateRequest + .getSoftwareModules()) { + if (!softwareModule.getModuleId().equals(module.getId())) { + continue; + } + assertFalse(softwareModule.getArtifacts().isEmpty()); + } + } + + @Test + @Description("Verfies that send cancel event works") + public void testSendCancelRequest() { + final CancelTargetAssignmentEvent cancelTargetAssignmentDistributionSetEvent = new CancelTargetAssignmentEvent( + CONTROLLER_ID, 1l, IpUtil.createAmqpUri("mytest")); + amqpMessageDispatcherService + .targetCancelAssignmentToDistributionSet(cancelTargetAssignmentDistributionSetEvent); + final Message sendMessage = createArgumentCapture( + cancelTargetAssignmentDistributionSetEvent.getTargetAdress().getHost()); + assertCancelMessage(sendMessage); + + } + + private void assertCancelMessage(final Message sendMessage) { + assertEventMessage(sendMessage); + final Long actionId = (Long) messageConverter.fromMessage(sendMessage); + assertEquals(actionId, Long.valueOf(1)); + assertEquals(EventTopic.CANCEL_DOWNLOAD, + sendMessage.getMessageProperties().getHeaders().get(MessageHeaderKey.TOPIC)); + + } + + private DownloadAndUpdateRequest assertDownloadAndInstallMessage(final Message sendMessage) { + assertEventMessage(sendMessage); + final DownloadAndUpdateRequest downloadAndUpdateRequest = (DownloadAndUpdateRequest) messageConverter + .fromMessage(sendMessage); + assertEquals(downloadAndUpdateRequest.getActionId(), Long.valueOf(1)); + assertEquals(EventTopic.DOWNLOAD_AND_INSTALL, + sendMessage.getMessageProperties().getHeaders().get(MessageHeaderKey.TOPIC)); + return downloadAndUpdateRequest; + + } + + /** + * @param sendMessage + */ + private void assertEventMessage(final Message sendMessage) { + assertNotNull(sendMessage); + + assertEquals(CONTROLLER_ID, sendMessage.getMessageProperties().getHeaders().get(MessageHeaderKey.THING_ID)); + assertEquals(MessageType.EVENT, sendMessage.getMessageProperties().getHeaders().get(MessageHeaderKey.TYPE)); + assertEquals(MessageProperties.CONTENT_TYPE_JSON, sendMessage.getMessageProperties().getContentType()); + } + + protected Message createArgumentCapture(final String exchange) { + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Message.class); + Mockito.verify(amqpMessageDispatcherService).sendMessage(eq(exchange), argumentCaptor.capture()); + return argumentCaptor.getValue(); + } + +} diff --git a/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java b/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java new file mode 100755 index 000000000..bea75e9d6 --- /dev/null +++ b/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java @@ -0,0 +1,432 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.amqp; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.hawkbit.api.HostnameResolver; +import org.eclipse.hawkbit.artifact.repository.ArtifactRepository; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash; +import org.eclipse.hawkbit.dmf.amqp.api.EventTopic; +import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey; +import org.eclipse.hawkbit.dmf.amqp.api.MessageType; +import org.eclipse.hawkbit.dmf.json.model.ActionStatus; +import org.eclipse.hawkbit.dmf.json.model.ActionUpdateStatus; +import org.eclipse.hawkbit.dmf.json.model.DownloadResponse; +import org.eclipse.hawkbit.dmf.json.model.TenantSecruityToken; +import org.eclipse.hawkbit.eventbus.event.TargetAssignDistributionSetEvent; +import org.eclipse.hawkbit.repository.ArtifactManagement; +import org.eclipse.hawkbit.repository.ControllerManagement; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.helper.SecurityTokenGeneratorHolder; +import org.eclipse.hawkbit.security.SecurityTokenGenerator; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.cache.Cache; +import org.springframework.http.HttpStatus; + +import com.google.common.eventbus.EventBus; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +@RunWith(MockitoJUnitRunner.class) +@Features("AMQP Controller Test") +@Stories("Tests the servcies for message handler and dispatcher") +public class AmqpMessageHandlerServiceTest { + + private static final String TENANT = "DEFAULT"; + + private AmqpMessageHandlerService amqpMessageHandlerService; + + private MessageConverter messageConverter; + + @Mock + private ControllerManagement controllerManagementMock; + + @Mock + private ArtifactManagement artifactManagementMock; + + @Mock + private AmqpControllerAuthentfication authenticationManagerMock; + + @Mock + private ArtifactRepository artifactRepositoryMock; + + @Mock + private Cache cacheMock; + + @Mock + private HostnameResolver hostnameResolverMock; + + @Mock + private EventBus eventBus; + + @Before + public void before() throws Exception { + amqpMessageHandlerService = new AmqpMessageHandlerService(); + amqpMessageHandlerService.setControllerManagement(controllerManagementMock); + messageConverter = new Jackson2JsonMessageConverter(); + final RabbitTemplate rabbitTemplate = new RabbitTemplate(); + rabbitTemplate.setMessageConverter(messageConverter); + amqpMessageHandlerService.setRabbitTemplate(rabbitTemplate); + amqpMessageHandlerService.setAuthenticationManager(authenticationManagerMock); + amqpMessageHandlerService.setArtifactManagement(artifactManagementMock); + amqpMessageHandlerService.setCache(cacheMock); + amqpMessageHandlerService.setHostnameResolver(hostnameResolverMock); + amqpMessageHandlerService.setEventBus(eventBus); + + } + + @Test(expected = IllegalArgumentException.class) + @Description("Tests not allowed content-type in message") + public void testWrongContentType() { + final MessageProperties messageProperties = new MessageProperties(); + messageProperties.setContentType("xml"); + final Message message = new Message(new byte[0], messageProperties); + amqpMessageHandlerService.onMessage(message, MessageType.THING_CREATED.name(), TENANT); + fail(); + } + + @Test + @Description("Tests the creation of a target/thing by calling the same method that incoming RabbitMQ messages would access.") + public void testCreateThing() { + final String knownThingId = "1"; + final MessageProperties messageProperties = createMessageProperties(MessageType.THING_CREATED); + messageProperties.setHeader(MessageHeaderKey.THING_ID, "1"); + final Message message = messageConverter.toMessage(new byte[0], messageProperties); + + // mock + final ArgumentCaptor targetIdCaptor = ArgumentCaptor.forClass(String.class); + final ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); + when(controllerManagementMock.findOrRegisterTargetIfItDoesNotexist(targetIdCaptor.capture(), + uriCaptor.capture())).thenReturn(null); + + // test + amqpMessageHandlerService.onMessage(message, MessageType.THING_CREATED.name(), TENANT); + + // verify + assertThat(targetIdCaptor.getValue()).isEqualTo(knownThingId); + assertThat(uriCaptor.getValue().toString()).isEqualTo("amqp://MyTest"); + + } + + @Test + @Description("Tests the creation of a thing without a 'reply to' header in message.") + public void testCreateThingWitoutReplyTo() { + final MessageProperties messageProperties = createMessageProperties(MessageType.THING_CREATED, null); + messageProperties.setHeader(MessageHeaderKey.THING_ID, "1"); + final Message message = messageConverter.toMessage("", messageProperties); + + try { + amqpMessageHandlerService.onMessage(message, MessageType.THING_CREATED.name(), TENANT); + fail("IllegalArgumentException was excepeted since no replyTo header was set"); + } catch (final IllegalArgumentException exception) { + // test ok - exception was excepted + } + + } + + @Test + @Description("Tests the creation of a target/thing without a thingID by calling the same method that incoming RabbitMQ messages would access.") + public void testCreateThingWithoutID() { + final MessageProperties messageProperties = createMessageProperties(MessageType.THING_CREATED); + final Message message = messageConverter.toMessage(new byte[0], messageProperties); + try { + amqpMessageHandlerService.onMessage(message, MessageType.THING_CREATED.name(), TENANT); + fail("IllegalArgumentException was excepeted since no thingID was set"); + } catch (final IllegalArgumentException exception) { + // test ok - exception was excepted + } + } + + @Test + @Description("Tests the call of the same method that incoming RabbitMQ messages would access with an unknown message type.") + public void testUnknownMessageType() { + final String type = "bumlux"; + final MessageProperties messageProperties = createMessageProperties(MessageType.THING_CREATED); + messageProperties.setHeader(MessageHeaderKey.THING_ID, ""); + final Message message = messageConverter.toMessage(new byte[0], messageProperties); + + try { + amqpMessageHandlerService.onMessage(message, type, TENANT); + fail("IllegalArgumentException was excepeted due to unknown message type"); + } catch (final IllegalArgumentException exception) { + // test ok - exception was excepted + } + } + + @Test + @Description("Tests a invalid message without event topic") + public void testInvalidEventTopic() { + final MessageProperties messageProperties = createMessageProperties(MessageType.EVENT); + final Message message = new Message(new byte[0], messageProperties); + try { + amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT); + fail(); + } catch (final IllegalArgumentException e) { + } + + try { + messageProperties.setHeader(MessageHeaderKey.TOPIC, "wrongTopic"); + amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT); + fail(); + } catch (final IllegalArgumentException e) { + } + + messageProperties.setHeader(MessageHeaderKey.TOPIC, EventTopic.CANCEL_DOWNLOAD.name()); + try { + amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT); + fail("IllegalArgumentException was excepeted because there was no event topic"); + } catch (final IllegalArgumentException exception) { + // test ok - exception was excepted + } + + } + + @Test + @Description("Tests the update of an action of a target without a exist action id") + public void testUpdateActionStatusWithoutActionId() { + final MessageProperties messageProperties = createMessageProperties(MessageType.EVENT); + messageProperties.setHeader(MessageHeaderKey.TOPIC, EventTopic.UPDATE_ACTION_STATUS.name()); + final ActionUpdateStatus actionUpdateStatus = new ActionUpdateStatus(); + actionUpdateStatus.setActionStatus(ActionStatus.DOWNLOAD); + final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(actionUpdateStatus, + messageProperties); + + try { + amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT); + fail("IllegalArgumentException was excepeted since no action id was set"); + } catch (final IllegalArgumentException exception) { + // test ok - exception was excepted + } + } + + @Test + @Description("Tests the update of an action of a target without a exist action id") + public void testUpdateActionStatusWithoutExistActionId() { + final MessageProperties messageProperties = createMessageProperties(MessageType.EVENT); + messageProperties.setHeader(MessageHeaderKey.TOPIC, EventTopic.UPDATE_ACTION_STATUS.name()); + final ActionUpdateStatus actionUpdateStatus = createActionUpdateStatus(ActionStatus.DOWNLOAD); + final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(actionUpdateStatus, + messageProperties); + + try { + amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT); + fail("IllegalArgumentException was excepeted since no action id was set"); + } catch (final IllegalArgumentException exception) { + // test ok - exception was excepted + } + + } + + @Test + @Description("Tests that an download request is denied for an artifact which does not exists") + public void authenticationRequestDeniedForArtifactWhichDoesNotExists() { + final MessageProperties messageProperties = createMessageProperties(MessageType.AUTHENTIFICATION); + final TenantSecruityToken securityToken = new TenantSecruityToken(TENANT, "123", "12345"); + final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(securityToken, + messageProperties); + + // test + final Message onMessage = amqpMessageHandlerService.onMessage(message, MessageType.AUTHENTIFICATION.name(), + TENANT); + + // verify + final DownloadResponse downloadResponse = (DownloadResponse) messageConverter.fromMessage(onMessage); + assertThat(downloadResponse).isNotNull(); + assertThat(downloadResponse.getResponseCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); + } + + @Test + @Description("Tests that an download request is denied for an artifact which is not assigned to the requested target") + public void authenticationRequestDeniedForArtifactWhichIsNotAssignedToTarget() { + final MessageProperties messageProperties = createMessageProperties(MessageType.AUTHENTIFICATION); + final TenantSecruityToken securityToken = new TenantSecruityToken(TENANT, "123", "12345"); + final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(securityToken, + messageProperties); + + final LocalArtifact localArtifactMock = mock(LocalArtifact.class); + when(artifactManagementMock.findFirstLocalArtifactsBySHA1(anyString())).thenReturn(localArtifactMock); + when(controllerManagementMock.getActionForDownloadByTargetAndSoftwareModule(anyObject(), anyObject())) + .thenThrow(EntityNotFoundException.class); + + // test + final Message onMessage = amqpMessageHandlerService.onMessage(message, MessageType.AUTHENTIFICATION.name(), + TENANT); + + // verify + final DownloadResponse downloadResponse = (DownloadResponse) messageConverter.fromMessage(onMessage); + assertThat(downloadResponse).isNotNull(); + assertThat(downloadResponse.getResponseCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); + } + + @Test + @Description("Tests that an download request is allowed for an artifact which exists and assigned to the requested target") + public void authenticationRequestAllowedForArtifactWhichExistsAndAssignedToTarget() throws MalformedURLException { + final MessageProperties messageProperties = createMessageProperties(MessageType.AUTHENTIFICATION); + final TenantSecruityToken securityToken = new TenantSecruityToken(TENANT, "123", "12345"); + final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(securityToken, + messageProperties); + + // mock + final LocalArtifact localArtifactMock = mock(LocalArtifact.class); + final Action actionMock = mock(Action.class); + final DbArtifact dbArtifactMock = mock(DbArtifact.class); + when(artifactManagementMock.findFirstLocalArtifactsBySHA1(anyString())).thenReturn(localArtifactMock); + when(controllerManagementMock.getActionForDownloadByTargetAndSoftwareModule(anyObject(), anyObject())) + .thenReturn(actionMock); + when(artifactManagementMock.loadLocalArtifactBinary(localArtifactMock)).thenReturn(dbArtifactMock); + when(dbArtifactMock.getArtifactId()).thenReturn("artifactId"); + when(dbArtifactMock.getSize()).thenReturn(1L); + when(dbArtifactMock.getHashes()).thenReturn(new DbArtifactHash("sha1", "md5")); + when(hostnameResolverMock.resolveHostname()).thenReturn(new URL("http://localhost")); + + // test + final Message onMessage = amqpMessageHandlerService.onMessage(message, MessageType.AUTHENTIFICATION.name(), + TENANT); + + // verify + final DownloadResponse downloadResponse = (DownloadResponse) messageConverter.fromMessage(onMessage); + assertThat(downloadResponse).isNotNull(); + assertThat(downloadResponse.getResponseCode()).isEqualTo(HttpStatus.OK.value()); + assertThat(downloadResponse.getArtifact().getSize()).isEqualTo(1L); + assertThat(downloadResponse.getDownloadUrl()).startsWith("http://localhost/api/v1/downloadserver/downloadId/"); + } + + @Test + @Description("Tests TODO") + public void lookupNextUpdateActionAfterFinished() throws IllegalArgumentException, IllegalAccessException { + + // Mock + final Action action = createActionWithTarget(22L, Status.FINISHED); + when(controllerManagementMock.findActionWithDetails(Matchers.any())).thenReturn(action); + when(controllerManagementMock.addUpdateActionStatus(Matchers.any(), Matchers.any())).thenReturn(action); + // for the test the same action can be used + final List actionList = new ArrayList(); + actionList.add(action); + when(controllerManagementMock.findActionByTargetAndActive(Matchers.any())).thenReturn(actionList); + + final List softwareModuleList = createSoftwareModuleList(); + when(controllerManagementMock.findSoftwareModulesByDistributionSet(Matchers.any())) + .thenReturn(softwareModuleList); + + final MessageProperties messageProperties = createMessageProperties(MessageType.EVENT); + messageProperties.setHeader(MessageHeaderKey.TOPIC, EventTopic.UPDATE_ACTION_STATUS.name()); + final ActionUpdateStatus actionUpdateStatus = createActionUpdateStatus(ActionStatus.FINISHED, 23L); + final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(actionUpdateStatus, + messageProperties); + + // test + amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT); + + // verify + final ArgumentCaptor captorTargetAssignDistributionSetEvent = ArgumentCaptor + .forClass(TargetAssignDistributionSetEvent.class); + verify(eventBus, times(1)).post(captorTargetAssignDistributionSetEvent.capture()); + final TargetAssignDistributionSetEvent targetAssignDistributionSetEvent = captorTargetAssignDistributionSetEvent + .getValue(); + + assertThat(targetAssignDistributionSetEvent.getControllerId()).isEqualTo("target1"); + assertThat(targetAssignDistributionSetEvent.getActionId()).isEqualTo(22L); + assertThat(targetAssignDistributionSetEvent.getSoftwareModules()).isEqualTo(softwareModuleList); + + } + + private ActionUpdateStatus createActionUpdateStatus(final ActionStatus status) { + return createActionUpdateStatus(status, 2l); + } + + private ActionUpdateStatus createActionUpdateStatus(final ActionStatus status, final Long id) { + final ActionUpdateStatus actionUpdateStatus = new ActionUpdateStatus(); + actionUpdateStatus.setActionId(id); + actionUpdateStatus.setSoftwareModuleId(Long.valueOf(2)); + actionUpdateStatus.setActionStatus(status); + return actionUpdateStatus; + } + + private MessageProperties createMessageProperties(final MessageType type) { + return createMessageProperties(type, "MyTest"); + } + + private MessageProperties createMessageProperties(final MessageType type, final String replyTo) { + final MessageProperties messageProperties = new MessageProperties(); + messageProperties.setHeader(MessageHeaderKey.TYPE, type.name()); + messageProperties.setHeader(MessageHeaderKey.TENANT, TENANT); + messageProperties.setContentType(MessageProperties.CONTENT_TYPE_JSON); + messageProperties.setReplyTo(replyTo); + return messageProperties; + } + + private List createSoftwareModuleList() { + final List softwareModuleList = new ArrayList(); + final SoftwareModule softwareModule = new SoftwareModule(); + softwareModule.setId(777L); + softwareModuleList.add(softwareModule); + return softwareModuleList; + } + + private Action createActionWithTarget(final Long targetId, final Status status) + throws IllegalArgumentException, IllegalAccessException { + // is needed for the creation of targets + initalizeSecurityTokenGenerator(); + + // Mock + final Action action = new Action(); + action.setId(targetId); + action.setStatus(status); + action.setTenant("DEFAULT"); + final Target target = new Target("target1"); + action.setTarget(target); + + return action; + } + + private void initalizeSecurityTokenGenerator() throws IllegalArgumentException, IllegalAccessException { + final SecurityTokenGeneratorHolder instance = SecurityTokenGeneratorHolder.getInstance(); + final Field[] fields = instance.getClass().getDeclaredFields(); + for (final Field field : fields) { + if (field.getType().isAssignableFrom(SecurityTokenGenerator.class)) { + field.setAccessible(true); + field.set(instance, new SecurityTokenGenerator()); + } + } + } +} diff --git a/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/util/PropertyBasedArtifactUrlHandlerTest.java b/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/util/PropertyBasedArtifactUrlHandlerTest.java new file mode 100755 index 000000000..fcafb23e4 --- /dev/null +++ b/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/util/PropertyBasedArtifactUrlHandlerTest.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.util; + +import static org.junit.Assert.assertEquals; + +import org.eclipse.hawkbit.AbstractIntegrationTestWithMongoDB; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.dmf.json.model.Artifact; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * + * + */ +@Features("Component Tests - Artifact URL Handler") +@Stories("Test to generate the artifact download URL") +public class PropertyBasedArtifactUrlHandlerTest extends AbstractIntegrationTestWithMongoDB { + + @Autowired + private ArtifactUrlHandler urlHandlerProperties; + @Autowired + private TenantAware tenantAware; + private LocalArtifact localArtifact; + private final String controllerId = "Test"; + + @Before + public void setup() { + final DistributionSet dsA = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + final SoftwareModule module = dsA.getModules().iterator().next(); + localArtifact = (LocalArtifact) TestDataUtil.generateArtifacts(artifactManagement, module.getId()).stream() + .findAny().get(); + } + + @Test + @Description("Tests generate the http download url") + public void testHttpUrl() { + final String url = urlHandlerProperties.getUrl(controllerId, localArtifact, Artifact.UrlProtocol.HTTP); + assertEquals("http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId + + "/softwaremodules/" + localArtifact.getSoftwareModule().getId() + "/artifacts/" + + localArtifact.getFilename(), url); + } + + @Test + @Description("Tests generate the https download url") + public void testHttpsUrl() { + final String url = urlHandlerProperties.getUrl(controllerId, localArtifact, Artifact.UrlProtocol.HTTPS); + assertEquals("https://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId + + "/softwaremodules/" + localArtifact.getSoftwareModule().getId() + "/artifacts/" + + localArtifact.getFilename(), url); + } + + @Test + @Description("Tests generate the coap download url") + public void testCoapUrl() { + final String url = urlHandlerProperties.getUrl(controllerId, localArtifact, Artifact.UrlProtocol.COAP); + + assertEquals("coap://127.0.0.1:5683/fw/" + tenantAware.getCurrentTenant() + "/" + controllerId + "/sha1/" + + localArtifact.getSha1Hash(), url); + } +} diff --git a/hawkbit-dmf-api/README.md b/hawkbit-dmf-api/README.md new file mode 100755 index 000000000..cdb6f8d50 --- /dev/null +++ b/hawkbit-dmf-api/README.md @@ -0,0 +1,4 @@ +# HawkBit Software Provisioning device management federation API + +This API is used for communicating with the HawkBit Software Provisioning server through AMQP. +It is used to integrate other device management system through a high performance protocol. \ No newline at end of file diff --git a/hawkbit-dmf-api/pom.xml b/hawkbit-dmf-api/pom.xml new file mode 100755 index 000000000..5660370e5 --- /dev/null +++ b/hawkbit-dmf-api/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + + org.eclipse.hawkbit + hawkbit-parent + 0.1.0-SNAPSHOT + + hawkbit-dmf-api + hawkBit :: Device Management Federation API + + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + diff --git a/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/AmqpSettings.java b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/AmqpSettings.java new file mode 100755 index 000000000..40ba04419 --- /dev/null +++ b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/AmqpSettings.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.amqp.api; + +/** + * Global constants for RabbitMQ settings. + * + * + * + */ +public final class AmqpSettings { + + public static final String DMF_EXCHANGE = "dmf.exchange"; + + private AmqpSettings() { + + } + +} diff --git a/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/EventTopic.java b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/EventTopic.java new file mode 100755 index 000000000..59b73467b --- /dev/null +++ b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/EventTopic.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.amqp.api; + +/** + * The event topics for the message headers. + * + * + * + */ +public enum EventTopic { + + /** + * Topic when sending and receiving a update status. + */ + UPDATE_ACTION_STATUS, + /** + * Topic when sending and receiving a download and install task. + */ + DOWNLOAD_AND_INSTALL, + /** + * Topic when sending and receiving a cancel download task. + */ + CANCEL_DOWNLOAD; + +} diff --git a/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/MessageHeaderKey.java b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/MessageHeaderKey.java new file mode 100755 index 000000000..005def4e7 --- /dev/null +++ b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/MessageHeaderKey.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.amqp.api; + +/** + * The constant of all amqp message header. + * + * + * + */ +public final class MessageHeaderKey { + + /** + * The message type. + */ + public static final String TYPE = "type"; + + /** + * The used tenant. + */ + public static final String TENANT = "tenant"; + + /** + * The name of the thing/target. + */ + public static final String THING_ID = "thingId"; + + /** + * The name of the sender who has send the message. + */ + public static final String SENDER = "sender"; + + /** + * The topic to handle events different. + */ + public static final String TOPIC = "topic"; + + /** + * The content type. + */ + public static final String CONTENT_TYPE = "content-type"; + + private MessageHeaderKey() { + + } + +} diff --git a/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/MessageType.java b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/MessageType.java new file mode 100755 index 000000000..8cca32b06 --- /dev/null +++ b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/MessageType.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.amqp.api; + +/** + * The amqp message types which can be handled. + * + * + * + */ +public enum MessageType { + + /** + * The event type. + */ + EVENT, + + /** + * the thing created type. + */ + THING_CREATED, + + /** + * The authentication type. + */ + AUTHENTIFICATION, + +} diff --git a/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/ActionStatus.java b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/ActionStatus.java new file mode 100755 index 000000000..883740354 --- /dev/null +++ b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/ActionStatus.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.json.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +/** + * The json message action status. + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public enum ActionStatus { + + DOWNLOAD, RETRIEVED, RUNNING, FINISHED, ERROR, WARNING; + +} diff --git a/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/ActionUpdateStatus.java b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/ActionUpdateStatus.java new file mode 100755 index 000000000..ac0926080 --- /dev/null +++ b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/ActionUpdateStatus.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.json.model; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * JSON representation of action update status. + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class ActionUpdateStatus { + @JsonProperty(required = true) + private Long actionId; + @JsonProperty + private Long softwareModuleId; + @JsonProperty(required = true) + private ActionStatus actionStatus; + @JsonProperty + private final List message = new ArrayList<>(); + + public Long getActionId() { + return actionId; + } + + public void setActionId(final Long actionId) { + this.actionId = actionId; + } + + public Long getSoftwareModuleId() { + return softwareModuleId; + } + + public void setSoftwareModuleId(final Long softwareModuleId) { + this.softwareModuleId = softwareModuleId; + } + + public ActionStatus getActionStatus() { + return actionStatus; + } + + public void setActionStatus(final ActionStatus actionStatus) { + this.actionStatus = actionStatus; + } + + public List getMessage() { + return message; + } + +} diff --git a/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/Artifact.java b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/Artifact.java new file mode 100755 index 000000000..c3f555c33 --- /dev/null +++ b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/Artifact.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.json.model; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * JSON representation of artifact. + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Artifact { + + /** + * Represented the supported protocols for artifact url's. + * + * + * + */ + public static enum UrlProtocol { + COAP, HTTP, HTTPS + } + + @JsonProperty + private String filename; + + @JsonProperty + private ArtifactHash hashes; + + @JsonProperty + private Long size; + + @JsonProperty + private Map urls = new HashMap<>(); + + public Map getUrls() { + return urls; + } + + public void setUrls(final Map urls) { + this.urls = urls; + } + + public String getFilename() { + return filename; + } + + public void setFilename(final String filename) { + this.filename = filename; + } + + public ArtifactHash getHashes() { + return hashes; + } + + public void setHashes(final ArtifactHash hashes) { + this.hashes = hashes; + } + + public Long getSize() { + return size; + } + + public void setSize(final Long size) { + this.size = size; + } + +} diff --git a/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/ArtifactHash.java b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/ArtifactHash.java new file mode 100755 index 000000000..5ddb48e4f --- /dev/null +++ b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/ArtifactHash.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.json.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * JSON representation of artifact hash. + * + * + * + * + */ +public class ArtifactHash { + + @JsonProperty + private String sha1; + + @JsonProperty + private String md5; + + /** + * Constructor. + * + * @param sha1 + * the sha1 hash + * @param md5 + * the md5 hash + */ + @JsonCreator + public ArtifactHash(@JsonProperty("sha1") final String sha1, @JsonProperty("md5") final String md5) { + super(); + this.sha1 = sha1; + this.md5 = md5; + } + + public void setSha1(final String sha1) { + this.sha1 = sha1; + } + + public void setMd5(final String md5) { + this.md5 = md5; + } + + public String getSha1() { + return sha1; + } + + public String getMd5() { + return md5; + } + +} diff --git a/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DownloadAndUpdateRequest.java b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DownloadAndUpdateRequest.java new file mode 100755 index 000000000..d31141879 --- /dev/null +++ b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DownloadAndUpdateRequest.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.json.model; + +import java.util.LinkedList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * JSON representation of download and update request. + * + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class DownloadAndUpdateRequest { + @JsonProperty + private Long actionId; + @JsonProperty + private final List softwareModules = new LinkedList(); + + public Long getActionId() { + return actionId; + } + + public void setActionId(final Long correlator) { + this.actionId = correlator; + } + + public List getSoftwareModules() { + return softwareModules; + } + + /** + * Add a Software module. + * + * @param createSoftwareModule + * the module + */ + public void addSoftwareModule(final SoftwareModule createSoftwareModule) { + softwareModules.add(createSoftwareModule); + + } +} diff --git a/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DownloadResponse.java b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DownloadResponse.java new file mode 100755 index 000000000..f81adbbb4 --- /dev/null +++ b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DownloadResponse.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.json.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The authentification response JSON representation. + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class DownloadResponse { + + @JsonProperty + private String downloadUrl; + + @JsonProperty + private Artifact artifact; + + @JsonProperty + private int responseCode; + + @JsonProperty + private String message; + + public String getMessage() { + return message; + } + + public void setMessage(final String message) { + this.message = message; + } + + public int getResponseCode() { + return responseCode; + } + + public void setResponseCode(final int responseCode) { + this.responseCode = responseCode; + } + + public String getDownloadUrl() { + return downloadUrl; + } + + public void setDownloadUrl(final String downloadUrl) { + this.downloadUrl = downloadUrl; + } + + public Artifact getArtifact() { + return artifact; + } + + public void setArtifact(final Artifact artifact) { + this.artifact = artifact; + } +} diff --git a/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/SoftwareModule.java b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/SoftwareModule.java new file mode 100755 index 000000000..2e553c051 --- /dev/null +++ b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/SoftwareModule.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.json.model; + +import java.util.LinkedList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * JSON representation of a software module. + * + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class SoftwareModule { + + @JsonProperty + private Long moduleId; + @JsonProperty + private String moduleType; + @JsonProperty + private String moduleVersion; + @JsonProperty + private List artifacts = new LinkedList(); + + public String getModuleType() { + return moduleType; + } + + public void setModuleType(final String moduleType) { + this.moduleType = moduleType; + } + + public String getModuleVersion() { + return moduleVersion; + } + + public void setModuleVersion(final String moduleVersion) { + this.moduleVersion = moduleVersion; + } + + public List getArtifacts() { + return artifacts; + } + + public Long getModuleId() { + return moduleId; + } + + @JsonIgnore + public void setModuleId(final Long moduleId) { + this.moduleId = moduleId; + } + + public void setArtifacts(final List artifacts) { + this.artifacts = artifacts; + } + +} diff --git a/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/TenantSecruityToken.java b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/TenantSecruityToken.java new file mode 100755 index 000000000..351684f5e --- /dev/null +++ b/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/TenantSecruityToken.java @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.json.model; + +import java.util.Map; +import java.util.TreeMap; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * JSON representation to authenticate a tenant. + * + * + * + */ + +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class TenantSecruityToken { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + public static final String COAP_AUTHORIZATION_HEADER = "Coap-Authorization"; + public static final String COAP_TOKEN_VALUE = "CoapToken"; + + @JsonProperty + private final String tenant; + @JsonProperty + private final String controllerId; + @JsonProperty(required = false) + private Map headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + @JsonProperty(required = false) + private final String sha1; + + /** + * Constructor. + * + * @param tenant + * the tenant for the security token + * @param controllerId + * the ID of the controller for the security token + * @param sha1 + * the sha1 of authentication + */ + @JsonCreator + public TenantSecruityToken(@JsonProperty("tenant") final String tenant, + @JsonProperty("controllerId") final String controllerId, @JsonProperty("sha1") final String sha1) { + this.tenant = tenant; + this.controllerId = controllerId; + this.sha1 = sha1; + } + + public String getTenant() { + return tenant; + } + + public String getControllerId() { + return controllerId; + } + + public Map getHeaders() { + return headers; + } + + public String getSha1() { + return sha1; + } + + /** + * Gets a header value. + * + * @param name + * of header + * @return the value + */ + public String getHeader(final String name) { + return headers.get(name); + } + + public void setHeaders(final Map headers) { + this.headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + this.headers.putAll(headers); + } + +} diff --git a/hawkbit-http-security/pom.xml b/hawkbit-http-security/pom.xml new file mode 100755 index 000000000..e5ee23de5 --- /dev/null +++ b/hawkbit-http-security/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + hawkbit-parent + 0.1.0-SNAPSHOT + org.eclipse.hawkbit + + hawkbit-http-security + hawkBit :: HTTP Security + + + + + org.eclipse.hawkbit + hawkbit-repository + ${project.version} + + + org.eclipse.hawkbit + hawkbit-security-core + ${project.version} + + + org.eclipse.hawkbit + hawkbit-security-integration + ${project.version} + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.easytesting + fest-assert-core + test + + + org.easytesting + fest-assert + test + + + ru.yandex.qatools.allure + allure-junit-adaptor + test + + + + \ No newline at end of file diff --git a/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/AbstractHttpControllerAuthenticationFilter.java b/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/AbstractHttpControllerAuthenticationFilter.java new file mode 100755 index 000000000..5782a48db --- /dev/null +++ b/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/AbstractHttpControllerAuthenticationFilter.java @@ -0,0 +1,180 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import java.io.IOException; +import java.util.Map; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.hawkbit.dmf.json.model.TenantSecruityToken; +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; +import org.springframework.util.AntPathMatcher; + +import com.google.common.collect.Iterators; +import com.google.common.collect.UnmodifiableIterator; + +/** + * An abstraction for all controller based security to parse the e.g. the tenant + * name from the URL and the controller ID from the URL to do security checks + * based on these information. + * + * + * + * + */ +public abstract class AbstractHttpControllerAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter { + + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractHttpControllerAuthenticationFilter.class); + + private static final String TENANT_PLACE_HOLDER = "tenant"; + private static final String CONTROLLER_ID_PLACE_HOLDER = "controllerId"; + + /** + * requestURIPathPattern the request URI path pattern in ANT style + * containing the placeholder key for retrieving the principal from the URI + * request. e.g."/{tenant}/controller/v1/{controllerId} + */ + private static final String CONTROLLER_REQUEST_ANT_PATTERN = "/{" + TENANT_PLACE_HOLDER + "}/controller/v1" + "/{" + + CONTROLLER_ID_PLACE_HOLDER + "}/**"; + + private static final String CONTROLLER_DL_REQUEST_ANT_PATTERN = "/{" + TENANT_PLACE_HOLDER + + "}/controller/artifacts/v1/**"; + protected SystemManagement systemManagement; + protected TenantAware tenantAware; + + private final AntPathMatcher pathExtractor; + + private PreAuthenficationFilter abstractControllerAuthenticationFilter; + + /** + * Constructor for sub-classes. + * + * @param systemManagement + * the system management service + * @param tenantAware + * the tenant aware service + */ + public AbstractHttpControllerAuthenticationFilter(final SystemManagement systemManagement, + final TenantAware tenantAware) { + this.systemManagement = systemManagement; + this.tenantAware = tenantAware; + pathExtractor = new AntPathMatcher(); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.security.web.authentication.preauth. + * AbstractPreAuthenticatedProcessingFilter + * #doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, + * javax.servlet.FilterChain) + */ + @Override + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) + throws IOException, ServletException { + + if (!(request instanceof HttpServletRequest)) { + chain.doFilter(request, response); + return; + } + + final TenantSecruityToken secruityToken = createTenantSecruityTokenVariables((HttpServletRequest) request); + if (secruityToken == null) { + chain.doFilter(request, response); + return; + } + abstractControllerAuthenticationFilter = createControllerAuthenticationFilter(); + if (abstractControllerAuthenticationFilter.isEnable(secruityToken) + && SecurityContextHolder.getContext().getAuthentication() == null) { + super.doFilter(request, response, chain); + } else { + chain.doFilter(request, response); + } + } + + protected abstract PreAuthenficationFilter createControllerAuthenticationFilter(); + + /** + * Extracts tenant and controllerId from the request URI as path variables. + * + * @param request + * the Http request to extract the path variables. + * @return the extracted {@link PathVariables} or {@code null} if the + * request does not match the pattern and no variables could be + * extracted + */ + protected TenantSecruityToken createTenantSecruityTokenVariables(final HttpServletRequest request) { + final String requestURI = request.getRequestURI(); + + if (pathExtractor.match(request.getContextPath() + CONTROLLER_REQUEST_ANT_PATTERN, requestURI)) { + LOGGER.debug("retrieving principal from URI request {}", requestURI); + final Map extractUriTemplateVariables = pathExtractor + .extractUriTemplateVariables(request.getContextPath() + CONTROLLER_REQUEST_ANT_PATTERN, requestURI); + final String controllerId = extractUriTemplateVariables.get(CONTROLLER_ID_PLACE_HOLDER); + final String tenant = extractUriTemplateVariables.get(TENANT_PLACE_HOLDER); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Parsed tenant {} and controllerId {} from path request {}", tenant, controllerId, + requestURI); + } + return createTenantSecruityTokenVariables(request, tenant, controllerId); + } else if (pathExtractor.match(request.getContextPath() + CONTROLLER_DL_REQUEST_ANT_PATTERN, requestURI)) { + LOGGER.debug("retrieving path variables from URI request {}", requestURI); + final Map extractUriTemplateVariables = pathExtractor.extractUriTemplateVariables( + request.getContextPath() + CONTROLLER_DL_REQUEST_ANT_PATTERN, requestURI); + final String tenant = extractUriTemplateVariables.get(TENANT_PLACE_HOLDER); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Parsed tenant {} from path request {}", tenant, requestURI); + } + return createTenantSecruityTokenVariables(request, tenant, "anonymous"); + } else { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("request {} does not match the path pattern {}, request gets ignored", requestURI, + CONTROLLER_REQUEST_ANT_PATTERN); + } + return null; + } + } + + private TenantSecruityToken createTenantSecruityTokenVariables(final HttpServletRequest request, + final String tenant, final String controllerId) { + final TenantSecruityToken secruityToken = new TenantSecruityToken(tenant, controllerId, ""); + final UnmodifiableIterator forEnumeration = Iterators.forEnumeration(request.getHeaderNames()); + forEnumeration.forEachRemaining(header -> secruityToken.getHeaders().put(header, request.getHeader(header))); + return secruityToken; + } + + @Override + protected Object getPreAuthenticatedPrincipal(final HttpServletRequest request) { + final TenantSecruityToken secruityToken = createTenantSecruityTokenVariables(request); + if (secruityToken == null) { + return null; + } + return abstractControllerAuthenticationFilter.getPreAuthenticatedPrincipal(secruityToken); + } + + @Override + protected Object getPreAuthenticatedCredentials(final HttpServletRequest request) { + final TenantSecruityToken secruityToken = createTenantSecruityTokenVariables(request); + if (secruityToken == null) { + return null; + } + return abstractControllerAuthenticationFilter.getPreAuthenticatedCredentials(secruityToken); + } + +} diff --git a/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/ControllerTenantAwareAuthenticationDetailsSource.java b/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/ControllerTenantAwareAuthenticationDetailsSource.java new file mode 100755 index 000000000..80d50112f --- /dev/null +++ b/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/ControllerTenantAwareAuthenticationDetailsSource.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.AuthenticationDetailsSource; +import org.springframework.util.AntPathMatcher; + +/** + * An {@link AuthenticationDetailsSource} implementation which retrieves the + * tenant from a request pattern {@link #TENANT_AWARE_CONTROLLER_PATTERN} and + * stores the retrieved tenant in the {@link TenantAwareAuthenticationDetails}. + * + * + */ +public class ControllerTenantAwareAuthenticationDetailsSource + implements AuthenticationDetailsSource { + + /** + * + */ + private static final String TENANT_AWARE_CONTROLLER_PATTERN = "/{tenant}/controller/**"; + private static final Logger LOGGER = LoggerFactory + .getLogger(ControllerTenantAwareAuthenticationDetailsSource.class); + private static final String TENANT_PLACE_HOLDER = "tenant"; + private final AntPathMatcher pathExtractor; + + /** + * + */ + public ControllerTenantAwareAuthenticationDetailsSource() { + pathExtractor = new AntPathMatcher(); + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.security.authentication.AuthenticationDetailsSource# + * buildDetails(java. lang.Object) + */ + @Override + public TenantAwareAuthenticationDetails buildDetails(final HttpServletRequest request) { + return new TenantAwareWebAuthenticationDetails(getTenantFromRequestUri(request), request.getRemoteAddr(), true); + } + + private String getTenantFromRequestUri(final HttpServletRequest request) { + final String requestURI = request.getRequestURI(); + LOGGER.debug("retrieving tenant from URI request {}", requestURI); + final String requestPathPattern = request.getContextPath() + TENANT_AWARE_CONTROLLER_PATTERN; + if (!pathExtractor.match(requestPathPattern, requestURI)) { + LOGGER.info("Controller request not matching tenant aware request pattern requestpath: {}, pattern {}", + requestURI, TENANT_AWARE_CONTROLLER_PATTERN); + return null; + } + final Map extractUriTemplateVariables = pathExtractor + .extractUriTemplateVariables(requestPathPattern, requestURI); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Parsed path variables {} using tenant {}", extractUriTemplateVariables, + extractUriTemplateVariables.get(TENANT_PLACE_HOLDER)); + } + return extractUriTemplateVariables.get(TENANT_PLACE_HOLDER); + } +} diff --git a/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/HttpControllerPreAuthenticateSecurityTokenFilter.java b/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/HttpControllerPreAuthenticateSecurityTokenFilter.java new file mode 100755 index 000000000..91a0cdfa6 --- /dev/null +++ b/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/HttpControllerPreAuthenticateSecurityTokenFilter.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import org.eclipse.hawkbit.repository.ControllerManagement; +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.tenancy.TenantAware; + +/** + * An pre-authenticated processing filter which extracts (if enabled through + * configuration) the possibility to authenticate a target based on its target + * security-token with the {@code Authorization} HTTP header. + * {@code Example Header: Authorization: TargetToken + * 5d8fSD54fdsFG98DDsa.} + * + * The {@code Authorization} header is a HTTP standard and reverse proxy or + * other proxies will keep the Authorization headers untouched instead of maybe + * custom headers which have then weird side-effects. Furthermore frameworks are + * aware of the sensitivity of the Authorization header and do not log it and + * store it somewhere. + * + * + * + */ +public class HttpControllerPreAuthenticateSecurityTokenFilter extends AbstractHttpControllerAuthenticationFilter { + + private final ControllerManagement controllerManagement; + + /** + * Constructor. + * + * @param systemManagement + * the system management service to retrieve configuration + * properties + * @param tenantAware + * the tenant aware service to get configuration for the specific + * tenant + * @param controllerManagement + * the controller management to retrieve the specific target + * security token to verify + */ + public HttpControllerPreAuthenticateSecurityTokenFilter(final SystemManagement systemManagement, + final TenantAware tenantAware, final ControllerManagement controllerManagement) { + super(systemManagement, tenantAware); + this.controllerManagement = controllerManagement; + } + + @Override + protected PreAuthenficationFilter createControllerAuthenticationFilter() { + return new ControllerPreAuthenticateSecurityTokenFilter(systemManagement, controllerManagement, tenantAware); + } + +} diff --git a/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/HttpControllerPreAuthenticatedGatewaySecurityTokenFilter.java b/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/HttpControllerPreAuthenticatedGatewaySecurityTokenFilter.java new file mode 100755 index 000000000..2b1a4d78f --- /dev/null +++ b/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/HttpControllerPreAuthenticatedGatewaySecurityTokenFilter.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.tenancy.TenantAware; + +/** + * Extract the {@code Authorization} header is a HTTP standard and reverse proxy + * or other proxies will keep the Authorization headers untouched instead of + * maybe custom headers which have then weird side-effects. Furthermore + * frameworks are aware of the sensitivity of the Authorization header and do + * not log it and store it somewhere. + * + * + * + */ +public class HttpControllerPreAuthenticatedGatewaySecurityTokenFilter + extends AbstractHttpControllerAuthenticationFilter { + + /** + * Constructor. + * + * @param systemManagement + * the system management service to retrieve configuration + * properties + * @param tenantAware + * the tenant aware service to get configuration for the specific + * tenant + */ + public HttpControllerPreAuthenticatedGatewaySecurityTokenFilter(final SystemManagement systemManagement, + final TenantAware tenantAware) { + super(systemManagement, tenantAware); + } + + @Override + protected PreAuthenficationFilter createControllerAuthenticationFilter() { + return new ControllerPreAuthenticatedGatewaySecurityTokenFilter(systemManagement, tenantAware); + } + +} diff --git a/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/HttpControllerPreAuthenticatedSecurityHeaderFilter.java b/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/HttpControllerPreAuthenticatedSecurityHeaderFilter.java new file mode 100755 index 000000000..3d6b43657 --- /dev/null +++ b/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/HttpControllerPreAuthenticatedSecurityHeaderFilter.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.tenancy.TenantAware; + +/** + * An pre-authenticated processing filter which extracts the principal from a + * request URI and the credential from a request header. + * + * + * + */ +public class HttpControllerPreAuthenticatedSecurityHeaderFilter extends AbstractHttpControllerAuthenticationFilter { + + private final String caCommonNameHeader; + private final String caAuthorityNameHeader; + + /** + * Creates a new {@link ControllerPreAuthenticatedSecurityHeaderFilter}, in + * case the HTTP request matches the given pattern the principal is parsed + * from the HTTP request with the given URI pattern, in case the URI pattern + * does not match the current request then only the existence of the + * configured header field is checked. + * + * @param caCommonNameHeader + * the http-header which holds the common-name of the certificate + * @param caAuthorityNameHeader + * the http-header which holds the ca-authority name of the + * certificate + * @param systemManagement + * the system management service to retrieve configuration + * properties to check if the header authentication is enabled + * for this tenant + * @param tenantAware + * the tenant aware service to get configuration for the specific + * tenant + */ + public HttpControllerPreAuthenticatedSecurityHeaderFilter(final String caCommonNameHeader, + final String caAuthorityNameHeader, final SystemManagement systemManagement, + final TenantAware tenantAware) { + super(systemManagement, tenantAware); + this.caCommonNameHeader = caCommonNameHeader; + this.caAuthorityNameHeader = caAuthorityNameHeader; + } + + @Override + protected PreAuthenficationFilter createControllerAuthenticationFilter() { + return new ControllerPreAuthenticatedSecurityHeaderFilter(caCommonNameHeader, caAuthorityNameHeader, + systemManagement, tenantAware); + } + +} diff --git a/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/HttpDownloadAuthenticationFilter.java b/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/HttpDownloadAuthenticationFilter.java new file mode 100755 index 000000000..4a4bf2f5c --- /dev/null +++ b/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/HttpDownloadAuthenticationFilter.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cache.Cache; +import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; + +/** + * Extracts download or upload id from the request URI secruity token and set + * the security context. + * + * + * + */ +public class HttpDownloadAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter { + + public static final String REQUEST_ID_REGEX_PATTERN = ".*\\/downloadId\\/.*"; + private static final Logger LOGGER = LoggerFactory.getLogger(HttpDownloadAuthenticationFilter.class); + + private final Pattern pattern; + private final Cache cache; + + /** + * Constructor. + * + * @param cache + * the cache + */ + public HttpDownloadAuthenticationFilter(final Cache cache) { + this.cache = cache; + this.pattern = Pattern.compile(REQUEST_ID_REGEX_PATTERN); + + } + + private Object getDownloadByUri(final String requestURI) { + final Matcher matcher = pattern.matcher(requestURI); + if (!matcher.matches()) { + return null; + } + LOGGER.debug("retrieving id from URI request {}", requestURI); + final String[] groups = requestURI.split("\\/"); + final String id = groups[groups.length - 1]; + if (id == null) { + return null; + } + return cache.get(id).get(); + } + + @Override + protected Object getPreAuthenticatedPrincipal(final HttpServletRequest request) { + return getDownloadByUri(request.getRequestURI()); + } + + @Override + protected Object getPreAuthenticatedCredentials(final HttpServletRequest request) { + return getDownloadByUri(request.getRequestURI()); + } +} diff --git a/hawkbit-http-security/src/test/java/org/eclipse/hawkbit/security/PreAuthTokenSourceTrustAuthenticationProviderTest.java b/hawkbit-http-security/src/test/java/org/eclipse/hawkbit/security/PreAuthTokenSourceTrustAuthenticationProviderTest.java new file mode 100755 index 000000000..f2ffe5c4c --- /dev/null +++ b/hawkbit-http-security/src/test/java/org/eclipse/hawkbit/security/PreAuthTokenSourceTrustAuthenticationProviderTest.java @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +@RunWith(MockitoJUnitRunner.class) +public class PreAuthTokenSourceTrustAuthenticationProviderTest { + + private static final String REQUEST_SOURCE_IP = "127.0.0.1"; + + private final PreAuthTokenSourceTrustAuthenticationProvider underTestWithoutSourceIpCheck = new PreAuthTokenSourceTrustAuthenticationProvider(); + private final PreAuthTokenSourceTrustAuthenticationProvider underTestWithSourceIpCheck = new PreAuthTokenSourceTrustAuthenticationProvider( + REQUEST_SOURCE_IP); + + @Mock + private TenantAwareWebAuthenticationDetails webAuthenticationDetailsMock; + + /** + * Testing in case the containing controllerId in the URI request path does + * not accord with the controllerId in the request header. + */ + @Test(expected = BadCredentialsException.class) + public void principalAndCredentialsNotTheSameThrowsAuthenticationException() { + final String principal = "controllerIdURL"; + final String credentials = "controllerIdHeader"; + final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(principal, + credentials); + token.setDetails(webAuthenticationDetailsMock); + + // test, should throw authentication exception + underTestWithoutSourceIpCheck.authenticate(token); + } + + /** + * Testing that the controllerId within the URI request path is the same + * with the controllerId within the request header and no source IP check is + * in place. + */ + @Test + public void principalAndCredentialsAreTheSameWithNoSourceIpCheckIsSuccessful() { + final String principal = "controllerId"; + final String credentials = "controllerId"; + final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(principal, + credentials); + token.setDetails(webAuthenticationDetailsMock); + + final Authentication authenticate = underTestWithoutSourceIpCheck.authenticate(token); + assertThat(authenticate.isAuthenticated()).isTrue(); + } + + /** + * Testing that the controllerId in the URI request match with the + * controllerId in the request header but the request are not coming from a + * trustful source. + */ + @Test(expected = InsufficientAuthenticationException.class) + public void priniciapAndCredentialsAreTheSameButSourceIpRequestNotMatching() { + final String remoteAddress = "192.168.1.1"; + final String principal = "controllerId"; + final String credentials = "controllerId"; + final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(principal, + credentials); + token.setDetails(webAuthenticationDetailsMock); + + when(webAuthenticationDetailsMock.getRemoteAddress()).thenReturn(remoteAddress); + + // test, should throw authentication exception + final Authentication authenticate = underTestWithSourceIpCheck.authenticate(token); + assertThat(authenticate.isAuthenticated()).isTrue(); + } + + /** + * Testing that the controllerId in the URI request match with the + * controllerId in the request header and the source Ip is matching the + * allowed remote IP address. + */ + @Test() + public void priniciapAndCredentialsAreTheSameAndSourceIpIsTrusted() { + final String principal = "controllerId"; + final String credentials = "controllerId"; + final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(principal, + credentials); + token.setDetails(webAuthenticationDetailsMock); + + when(webAuthenticationDetailsMock.getRemoteAddress()).thenReturn(REQUEST_SOURCE_IP); + + // test, should throw authentication exception + final Authentication authenticate = underTestWithSourceIpCheck.authenticate(token); + assertThat(authenticate.isAuthenticated()).isTrue(); + } + + @Test() + public void priniciapAndCredentialsAreTheSameAndSourceIpIsWithinList() { + final String[] trustedIPAddresses = new String[] { "192.168.1.1", "192.168.1.2", REQUEST_SOURCE_IP, + "192.168.1.3" }; + final String principal = "controllerId"; + final String credentials = "controllerId"; + final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(principal, + credentials); + token.setDetails(webAuthenticationDetailsMock); + + when(webAuthenticationDetailsMock.getRemoteAddress()).thenReturn(REQUEST_SOURCE_IP); + + final PreAuthTokenSourceTrustAuthenticationProvider underTestWithList = new PreAuthTokenSourceTrustAuthenticationProvider( + trustedIPAddresses); + + // test, should throw authentication exception + final Authentication authenticate = underTestWithList.authenticate(token); + assertThat(authenticate.isAuthenticated()).isTrue(); + } + + @Test(expected = InsufficientAuthenticationException.class) + public void principalAndCredentialsAreTheSameSourceIpListNotMatches() { + final String[] trustedIPAddresses = new String[] { "192.168.1.1", "192.168.1.2", "192.168.1.3" }; + final String principal = "controllerId"; + final String credentials = "controllerId"; + final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(principal, + credentials); + token.setDetails(webAuthenticationDetailsMock); + + when(webAuthenticationDetailsMock.getRemoteAddress()).thenReturn(REQUEST_SOURCE_IP); + + final PreAuthTokenSourceTrustAuthenticationProvider underTestWithList = new PreAuthTokenSourceTrustAuthenticationProvider( + trustedIPAddresses); + + // test, should throw authentication exception + final Authentication authenticate = underTestWithList.authenticate(token); + assertThat(authenticate.isAuthenticated()).isTrue(); + } +} diff --git a/hawkbit-repository/README.md b/hawkbit-repository/README.md new file mode 100755 index 000000000..6c1e35c5d --- /dev/null +++ b/hawkbit-repository/README.md @@ -0,0 +1,12 @@ +# hawkBit metadata repository + +The repository is in charge for managing the meta data of the update server, e.g. provisioning targets, distribution sets, software modules etc. + +# Build + +[indent=0] +---- + $ mvn clean install +---- + +Note, in order to build correctly in your IDE, you have to add ./target/generated-sources/apt to your build path. \ No newline at end of file diff --git a/hawkbit-repository/pom.xml b/hawkbit-repository/pom.xml new file mode 100755 index 000000000..930d1bcc6 --- /dev/null +++ b/hawkbit-repository/pom.xml @@ -0,0 +1,254 @@ + + + 4.0.0 + + org.eclipse.hawkbit + 0.1.0-SNAPSHOT + hawkbit-parent + + hawkbit-repository + hawkBit :: Repository + + + + + + com.ethlo.eclipselink.tools + http://ethlo.com/maven + + + + + + com.ethlo.eclipselink.tools + http://ethlo.com/maven + + + + + + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + + + org.eclipse.hawkbit + hawkbit-artifact-repository-mongo + ${project.version} + + + org.eclipse.hawkbit + hawkbit-core + ${project.version} + + + org.eclipse.hawkbit + hawkbit-security-core + ${project.version} + + + org.hibernate + hibernate-validator + + + com.google.guava + guava + 18.0 + + + net._01001111 + jlorem + + + org.springframework.boot + spring-boot + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.eclipse.persistence + org.eclipse.persistence.jpa + + + org.springframework.security + spring-security-core + + + org.springframework.hateoas + spring-hateoas + + + org.flywaydb + flyway-core + + + + + com.h2database + h2 + test + + + org.mariadb.jdbc + mariadb-java-client + test + + + javax.el + javax.el-api + test + + + org.springframework + spring-context-support + test + + + org.eclipse.hawkbit + hawkbit-rest-api + ${project.version} + test + + + ru.yandex.qatools.allure + allure-junit-adaptor + test + + + org.springframework.data + spring-data-rest-webmvc + test + + + org.springframework.security + spring-security-aspects + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework + spring-test + test + + + org.easytesting + fest-assert-core + test + + + org.easytesting + fest-assert + test + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + test + + + org.springframework.security + spring-security-config + test + + + org.springframework.security + spring-security-web + test + + + cz.jirutka.rsql + rsql-parser + + + + + + + + org.bsc.maven + maven-processor-plugin + + + process + + process + + generate-sources + + + org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor + + + + + + + org.hibernate + hibernate-jpamodelgen + ${hibernate.version} + + + + + + com.ethlo.persistence.tools + eclipselink-maven-plugin + 1.1-SNAPSHOT + + + process-classes + + weave + + + + + + org.eclipse.persistence + org.eclipse.persistence.jpa + ${eclipselink.version} + + + org.javassist + javassist + 3.20.0-GA + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + diff --git a/hawkbit-repository/src/fbExcludeFilter.xml b/hawkbit-repository/src/fbExcludeFilter.xml new file mode 100755 index 000000000..bd85e5e73 --- /dev/null +++ b/hawkbit-repository/src/fbExcludeFilter.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/Constants.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/Constants.java new file mode 100755 index 000000000..5f3f772b0 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/Constants.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +/** + * A constant class which holds only static constants used within the SP server. + * + * + * + * + */ +public final class Constants { + + /** + * Defines the maximum entries for SQL in-statement. Many database have + * limit of elements within an SQL in-statement, e.g. Oracle with maximum + * 1000 elements. So everywhere we are using in-statements in our SQL + * queries we need to split these elements for the in-statements by this + * number. + */ + public static final int MAX_ENTRIES_IN_STATEMENT = 999; + + /** + * constant class only private constructor. + */ + private Constants() { + + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/MultiTenantJpaTransactionManager.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/MultiTenantJpaTransactionManager.java new file mode 100755 index 000000000..2675510c4 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/MultiTenantJpaTransactionManager.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import javax.persistence.EntityManager; +import javax.transaction.Transaction; + +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.repository.exception.TenantNotExistException; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.eclipse.persistence.config.PersistenceUnitProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.orm.jpa.EntityManagerHolder; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * {@link JpaTransactionManager} that sets the + * {@link TenantAware#getCurrentTenant()} in the eclipselink session. This has + * to be done in eclipselink after a {@link Transaction} has been started. + * + * + * + */ +public class MultiTenantJpaTransactionManager extends JpaTransactionManager { + private static final long serialVersionUID = 1L; + + @Autowired + private transient TenantAware tenantAware; + + @Override + protected void doBegin(final Object transaction, final TransactionDefinition definition) { + super.doBegin(transaction, definition); + + final EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager + .getResource(getEntityManagerFactory()); + final EntityManager em = emHolder.getEntityManager(); + + if (!definition.getName().startsWith(SystemManagement.class.getCanonicalName() + ".findTenants") + && !definition.getName().startsWith(SystemManagement.class.getCanonicalName() + ".deleteTenant") + && !definition.getName() + .startsWith(SystemManagement.class.getCanonicalName() + ".currentTenantKeyGenerator")) { + + final String currentTenant = tenantAware.getCurrentTenant(); + if (currentTenant == null) { + throw new TenantNotExistException("Tenant Unknown. Canceling transaction."); + } + + em.setProperty(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT, currentTenant.toUpperCase()); + } + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/RepositoryApplicationConfiguration.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/RepositoryApplicationConfiguration.java new file mode 100755 index 000000000..d0450122c --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/RepositoryApplicationConfiguration.java @@ -0,0 +1,178 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.hawkbit.aspects.SpExceptionMappingAspect; +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.repository.model.helper.CacheManagerHolder; +import org.eclipse.hawkbit.repository.model.helper.PollConfigurationHelper; +import org.eclipse.hawkbit.repository.model.helper.SecurityTokenGeneratorHolder; +import org.eclipse.hawkbit.repository.model.helper.SystemManagementHolder; +import org.eclipse.hawkbit.repository.model.helper.TenantAwareHolder; +import org.eclipse.hawkbit.security.SecurityTokenGenerator; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter; +import org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; + +/** + * General configuration for the SP Repository. + * + * + * + * + * + */ +@EnableJpaRepositories(basePackages = { "org.eclipse.hawkbit.repository" }) +@EnableTransactionManagement +@EnableJpaAuditing +@EnableAspectJAutoProxy +@Configuration +@ComponentScan +@EnableAutoConfiguration +public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { + + /** + * @return simple default {@link AsyncUncaughtExceptionHandler} + * implementation + * @Bean public SimpleAsyncUncaughtExceptionHandler + * simpleAsyncUncaughtExceptionHandler() { return new + * SimpleAsyncUncaughtExceptionHandler(); } + * + * /** + * @return the {@link PollConfigurationHelper} singleton bean which holds + * the polling and polling overdue configuration and make it + * accessible in beans which cannot not be autowired or retrieve + * environment variables due {@link EnvironmentAware} interface. + */ + @Bean + public PollConfigurationHelper pollConfigurationHelper() { + return PollConfigurationHelper.getInstance(); + } + + /** + * @return the {@link SystemManagementHolder} singleton bean which holds the + * current {@link SystemManagement} service and make it accessible + * in beans which cannot access the service directly, e.g. JPA + * entities. + */ + @Bean + public SystemManagementHolder systemManagementHolder() { + return SystemManagementHolder.getInstance(); + } + + /** + * @return the {@link TenantAwareHolder} singleton bean which holds the + * current {@link TenantAware} service and make it accessible in + * beans which cannot access the service directly, e.g. JPA + * entities. + */ + @Bean + public TenantAwareHolder tenantAwareHolder() { + return TenantAwareHolder.getInstance(); + } + + /** + * @return the {@link SecurityTokenGeneratorHolder} singleton bean which + * holds the current {@link SecurityTokenGenerator} service and make + * it accessible in beans which cannot access the service via + * injection + */ + @Bean + public SecurityTokenGeneratorHolder securityTokenGeneratorHolder() { + return SecurityTokenGeneratorHolder.getInstance(); + } + + /** + * @return the singleton instance of the {@link CacheManagerHolder} + */ + @Bean + public CacheManagerHolder cacheManagerHolder() { + return CacheManagerHolder.getInstance(); + } + + /** + * Defines the validation processor bean. + * + * @return the {@link MethodValidationPostProcessor} + */ + @Bean + public MethodValidationPostProcessor methodValidationPostProcessor() { + return new MethodValidationPostProcessor(); + } + + /** + * @return {@link SpExceptionMappingAspect} aspect bean + */ + @Bean + public SpExceptionMappingAspect createRepositoryExceptionHandlerAdvice() { + return new SpExceptionMappingAspect(); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration# + * createJpaVendorAdapter() + */ + @Override + protected AbstractJpaVendorAdapter createJpaVendorAdapter() { + return new EclipseLinkJpaVendorAdapter(); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration# + * getVendorProperties() + */ + @Override + protected Map getVendorProperties() { + + final Map properties = new HashMap(); + // Turn off dynamic weaving to disable LTW lookup in static weaving mode + properties.put("eclipselink.weaving", "false"); + // needed for reports + properties.put("eclipselink.jdbc.allow-native-sql-queries", "true"); + // flyway + properties.put("eclipselink.ddl-generation", "none"); + + properties.put("eclipselink.persistence-context.flush-mode", "auto"); + + return properties; + } + + /** + * {@link MultiTenantJpaTransactionManager} bean. + * + * @see org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration#transactionManager() + * @return a new {@link PlatformTransactionManager} + */ + @Override + @Bean + public PlatformTransactionManager transactionManager() { + return new MultiTenantJpaTransactionManager(); + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/aspects/SpExceptionMappingAspect.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/aspects/SpExceptionMappingAspect.java new file mode 100755 index 000000000..90fc95d8d --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/aspects/SpExceptionMappingAspect.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.aspects; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.transaction.TransactionManager; + +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.eclipse.hawkbit.exception.GenericSpServerException; +import org.eclipse.hawkbit.repository.exception.ConcurrentModificationException; +import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.Ordered; +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.security.access.AccessDeniedException; + +/** + * {@link Aspect} catches persistence exceptions and wraps them to custom + * specific exceptions Additionally it checks and prevents access to certain + * packages. Logging aspect which logs the call stack + * + * + * + */ +@Aspect +public class SpExceptionMappingAspect implements Ordered { + private static final Logger LOG = LoggerFactory.getLogger(SpExceptionMappingAspect.class); + + private static final Map EXCEPTION_MAPPING = new HashMap(); + + /** + * this is required to enable a certain order of exception and to select the + * most specific mappable exception according to the type hierarchy of the + * exception. + */ + private static final List> MAPPED_EXCEPTION_ORDER = new ArrayList>(); + + static { + MAPPED_EXCEPTION_ORDER.add(DuplicateKeyException.class); + MAPPED_EXCEPTION_ORDER.add(DataIntegrityViolationException.class); + MAPPED_EXCEPTION_ORDER.add(ConcurrencyFailureException.class); + MAPPED_EXCEPTION_ORDER.add(AccessDeniedException.class); + + EXCEPTION_MAPPING.put(DuplicateKeyException.class.getName(), EntityAlreadyExistsException.class.getName()); + + EXCEPTION_MAPPING.put(ConcurrencyFailureException.class.getName(), + ConcurrentModificationException.class.getName()); + EXCEPTION_MAPPING.put(AccessDeniedException.class.getName(), InsufficientPermissionException.class.getName()); + } + + /** + * catch exceptions of the {@link TransactionManager} and wrap them to + * custom exceptions. + * + * @param ex + * the thrown and catched exception + * @throws Throwable + */ + @AfterThrowing(pointcut = "( execution( * org.springframework.transaction..*.*(..)) " + + " || execution( * org.eclipse.hawkbit.repository.*.*(..)) " + + " || execution( * org.eclipse.hawkbit.controller.*.*(..)) " + + " || execution( * org.eclipse.hawkbit.service.*.*(..)) )", throwing = "ex") + public void catchAndWrapJpaExceptionsService(final Exception ex) throws Throwable { + final Class exClass = ex.getClass(); + Exception newEx = ex; + LOG.trace("exception occured", ex); + for (final Class mappedEx : MAPPED_EXCEPTION_ORDER) { + + if (mappedEx.isAssignableFrom(exClass)) { + if (!EXCEPTION_MAPPING.containsKey(mappedEx.getName())) { + LOG.error("there is no mapping configured for exception class {}", mappedEx.getName()); + newEx = new GenericSpServerException(ex); + } else { + newEx = (Exception) Class.forName(EXCEPTION_MAPPING.get(mappedEx.getName())) + .getConstructor(Throwable.class).newInstance(ex); + } + break; + } + } + LOG.trace("mapped exception {} to {}", ex.getClass(), newEx.getClass()); + throw newEx; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.core.Ordered#getOrder() + */ + @Override + public int getOrder() { + return 1; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/cache/CacheField.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/cache/CacheField.java new file mode 100755 index 000000000..8909c1125 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/cache/CacheField.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.cache; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.eclipse.hawkbit.eventbus.CacheFieldEntityListener; +import org.springframework.cache.CacheManager; +import org.springframework.data.annotation.Transient; + +/** + * Marks an field within a JPA entity as transient and this field should be + * loaded from a configured {@link CacheManager} by using the JPA entity + * listeners. + * + * + * + * @see CacheFieldEntityListener + */ +@Target({ FIELD }) +@Retention(RUNTIME) +@Transient +public @interface CacheField { + + /** + * @return the cache key name for this cacheable field which is used to + * store the value. + */ + String key(); + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/cache/CacheKeys.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/cache/CacheKeys.java new file mode 100755 index 000000000..8eca2744a --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/cache/CacheKeys.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.cache; + +import org.eclipse.hawkbit.eventbus.CacheFieldEntityListener; + +/** + * Constants for cache keys used in multiple classes. + * + * + * + * + * @see CacheFieldEntityListener + * @see CacheWriteNotify + * @see CacheField + * + */ +public final class CacheKeys { + + /** + * The cache key name for the {@link UpdateActionStatus} download progress + * event. + */ + public static final String DOWNLOAD_PROGRESS_PERCENT = "download.progress.percent"; + + /** + * utility class only private constructor. + */ + private CacheKeys() { + + } + + /** + * calculates the cache key for a specific entity. The cache key must be + * different for each different identifiable entity by its ID. + * + * @param entityId + * the ID of the entity to build the specific cache key for this + * entity + * @param cacheKey + * the cache key for the field to be cached + * @return the combined cache key based on the given {@code entityId} and + * {@code cacheKey} + */ + public static String entitySpecificCacheKey(final String entityId, final String cacheKey) { + return entityId + "." + cacheKey; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/cache/CacheWriteNotify.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/cache/CacheWriteNotify.java new file mode 100755 index 000000000..812f4ca85 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/cache/CacheWriteNotify.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.cache; + +import org.eclipse.hawkbit.eventbus.event.DownloadProgressEvent; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; + +import com.google.common.eventbus.EventBus; + +/** + * An service which combines the functionality for functional use cases to write + * into the cache an notify the writing to the cache to the {@link EventBus}. + * + * + * + */ +@Service +public class CacheWriteNotify { + + /** + * + */ + private static final int DOWNLOAD_PROGRESS_MAX = 100; + + @Autowired + private CacheManager cacheManager; + + @Autowired + private EventBus eventBus; + + @Autowired + private TenantAware tenantAware; + + /** + * writes the download progress in percentage into the cache + * {@link CacheKeys#DOWNLOAD_PROGRESS_PERCENT} and notifies the + * {@link EventBus} with a {@link DownloadProgressEvent}. + * + * @param statusId + * the ID of the {@link ActionStatus} + * @param progressPercent + * the progress in percentage which must be between 0-100 + */ + public void downloadProgressPercent(final long statusId, final int progressPercent) { + + final Cache cache = cacheManager.getCache(Action.class.getName()); + final String cacheKey = CacheKeys.entitySpecificCacheKey(String.valueOf(statusId), + CacheKeys.DOWNLOAD_PROGRESS_PERCENT); + if (progressPercent < DOWNLOAD_PROGRESS_MAX) { + cache.put(cacheKey, progressPercent); + } else { + // in case we reached progress 100 delete the cache value again + // because otherwise he will + // keep there forever + cache.evict(cacheKey); + } + + eventBus.post(new DownloadProgressEvent(tenantAware.getCurrentTenant(), statusId, progressPercent)); + } + + /** + * @param cacheManager + * the cacheManager to set + */ + void setCacheManager(final CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + /** + * @param eventBus + * the eventBus to set + */ + void setEventBus(final EventBus eventBus) { + this.eventBus = eventBus; + } + + /** + * @param tenantAware + * the tenantAware to set + */ + void setTenantAware(final TenantAware tenantAware) { + this.tenantAware = tenantAware; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/CacheFieldEntityListener.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/CacheFieldEntityListener.java new file mode 100755 index 000000000..7275cb4ec --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/CacheFieldEntityListener.java @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus; + +import java.io.Serializable; +import java.lang.reflect.Field; + +import javax.persistence.PostLoad; +import javax.persistence.PostRemove; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.eclipse.hawkbit.cache.CacheField; +import org.eclipse.hawkbit.cache.CacheKeys; +import org.eclipse.hawkbit.repository.model.helper.CacheManagerHolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cache.Cache; +import org.springframework.cache.Cache.ValueWrapper; +import org.springframework.cache.CacheManager; +import org.springframework.hateoas.Identifiable; + +/** + * An JPA entity listener which enriches the JPA entity fields which are + * annotated with {@link CacheField} with values from the {@link CacheManager}. + * Only JPA entities which are implementing {@link Identifiable} can be handled + * by this entity listener cause the cache keys are calculated with the ID of + * the entity. + * + * + * + * + */ +public class CacheFieldEntityListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(CacheFieldEntityListener.class); + + /** + * enriches the JPA entities after loading the entity from the SQL database. + * + * @param target + * the target which has been loaded from the database + */ + @PostLoad + public void postLoad(final Object target) { + if (target instanceof Identifiable) { + final CacheManager cacheManager = CacheManagerHolder.getInstance().getCacheManager(); + @SuppressWarnings("rawtypes") + final String id = ((Identifiable) target).getId().toString(); + final Class type = target.getClass(); + findCacheFields(type, id, new CacheFieldCallback() { + @Override + public void fromCache(final Field field, final String cacheKey, final Serializable id) + throws IllegalAccessException { + final Cache cache = cacheManager.getCache(type.getName()); + final ValueWrapper valueWrapper = cache + .get(CacheKeys.entitySpecificCacheKey(id.toString(), cacheKey)); + if (valueWrapper != null && valueWrapper.get() != null) { + FieldUtils.writeField(field, target, valueWrapper.get(), true); + } + } + }); + } + } + + /** + * deletes the associated cache fields from the cache when deleted the + * entity from the database. + * + * @param target + * the entity which has been deleted + */ + @PostRemove + public void postDelete(final Object target) { + if (target instanceof Identifiable) { + final CacheManager cacheManager = CacheManagerHolder.getInstance().getCacheManager(); + @SuppressWarnings("rawtypes") + final String id = ((Identifiable) target).getId().toString(); + final Class type = target.getClass(); + findCacheFields(type, id, new CacheFieldCallback() { + @Override + public void fromCache(final Field field, final String cacheKey, final Serializable id) + throws IllegalAccessException { + final Cache cache = cacheManager.getCache(type.getName()); + cache.evict(CacheKeys.entitySpecificCacheKey(id.toString(), cacheKey)); + } + }); + } + } + + private void findCacheFields(final Class type, final Serializable id, + final CacheFieldCallback callback) { + final Field[] declaredFields = type.getDeclaredFields(); + for (final Field field : declaredFields) { + if (field.getAnnotation(CacheField.class) != null) { + try { + final CacheField annotation = field.getAnnotation(CacheField.class); + callback.fromCache(field, annotation.key(), id); + } catch (final IllegalAccessException e) { + LOGGER.error("cannot access the field {} for the entity {}, ignoring the cacheable field", field, + type, e); + } + } + } + } + + private interface CacheFieldCallback { + /** + * callback methods which is called by the + * {@link CacheFieldEntityListener#findCacheFields(Class, Serializable, CacheFieldCallback)} + * in case a field is annotated with {@link CacheField}. + * + * @param field + * the field which is annotaed with {@link CacheField} + * @param cacheKey + * the configured cache key in the annotation + * {@link CacheField#key()} + * @param id + * the ID of the entity + * @throws IllegalAccessException + * in case the field cannot be accessed + */ + void fromCache(final Field field, final String cacheKey, Serializable id) throws IllegalAccessException; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/EntityChangeEventListener.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/EntityChangeEventListener.java new file mode 100755 index 000000000..a62d9265b --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/EntityChangeEventListener.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus; + +import java.util.Collection; + +import javax.persistence.EntityManager; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.eclipse.hawkbit.eventbus.event.TargetCreatedEvent; +import org.eclipse.hawkbit.eventbus.event.TargetDeletedEvent; +import org.eclipse.hawkbit.eventbus.event.TargetInfoUpdateEvent; +import org.eclipse.hawkbit.repository.TargetRepository; +import org.eclipse.hawkbit.repository.model.BaseEntity; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetInfo; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.google.common.eventbus.EventBus; + +/** + * An aspect implementation which wraps the necessary repository services for + * saving {@link BaseEntity}s to publish create or update events. + * + * + * + * + */ +@Service +@Aspect +public class EntityChangeEventListener { + + @Autowired + private EventBus eventBus; + + @Autowired + private TenantAware tenantAware; + + @Autowired + private EntityManager entityManager; + + /** + * In case the a {@link Target} is created a corresponding + * {@link TargetInfo} is created as well. We need the {@link TargetInfo} + * information in the target created event. So we are listening to the + * {@link TargetInfo} creation to indicate if an Target has been created. + * + * @param joinpoint + * the aspect join point + * @return the object of the {@link ProceedingJoinPoint#proceed()} + * @throws Throwable + * in case exception happens in the + * {@link ProceedingJoinPoint#proceed()} + */ + @Around("execution(* org.eclipse.hawkbit.repository.TargetInfoRepository.save(..))") + public Object targetCreated(final ProceedingJoinPoint joinpoint) throws Throwable { + final boolean isNew = isTargetInfoNew(joinpoint.getArgs()[0]); + final Object result = joinpoint.proceed(); + if (result instanceof TargetInfo) { + if (isNew) { + // we need to flush here because seems like eclipselink + // implementation setting the ID of the target no immediately + // otherwise. + entityManager.flush(); + notifyTargetCreated(entityManager.merge(entityManager.merge(((TargetInfo) result).getTarget()))); + } else { + notifyTargetInfoChanged((TargetInfo) result); + } + } + return result; + } + + /** + * Proxy method around the delete method of the {@link TargetRepository} to + * notify the {@link TargetDeletedEvent} in case targets has been deleted. + * + * @param joinpoint + * the aspect join point + * @return the object of the {@link ProceedingJoinPoint#proceed()} + * @throws Throwable + * in case exception happens in the + * {@link ProceedingJoinPoint#proceed()} + */ + @Around("execution(* org.eclipse.hawkbit.repository.TargetRepository.deleteByIdIn(..))") + public Object targetDeletedById(final ProceedingJoinPoint joinpoint) throws Throwable { + final String currentTenant = tenantAware.getCurrentTenant(); + final Object result = joinpoint.proceed(); + final Collection targetIds = (Collection) joinpoint.getArgs()[0]; + targetIds.forEach(targetId -> notifyTargetDeleted(currentTenant, targetId)); + return result; + } + + /** + * Proxy method around the delete method of the {@link TargetRepository} to + * notify the {@link TargetDeletedEvent} in case targets has been deleted. + * + * @param joinpoint + * the aspect join point + * @return the object of the {@link ProceedingJoinPoint#proceed()} + * @throws Throwable + * in case exception happens in the + * {@link ProceedingJoinPoint#proceed()} + */ + @SuppressWarnings("unchecked") + @Around("execution(* org.eclipse.hawkbit.repository.TargetRepository.delete(..))") + public Object targetDeleted(final ProceedingJoinPoint joinpoint) throws Throwable { + final String currentTenant = tenantAware.getCurrentTenant(); + final Object result = joinpoint.proceed(); + final Object param = joinpoint.getArgs()[0]; + // delete by id + if (param instanceof Long) { + notifyTargetDeleted(currentTenant, (Long) param); + } else if (param instanceof Target) { + notifyTargetDeleted(currentTenant, ((Target) param).getId()); + } else if (param instanceof Iterable) { + ((Iterable) param).forEach(target -> notifyTargetDeleted(currentTenant, target.getId())); + } + return result; + } + + private void notifyTargetCreated(final Target t) { + eventBus.post(new TargetCreatedEvent(t)); + } + + private void notifyTargetInfoChanged(final TargetInfo targetInfo) { + eventBus.post(new TargetInfoUpdateEvent(targetInfo)); + } + + private void notifyTargetDeleted(final String tenant, final Long targetId) { + eventBus.post(new TargetDeletedEvent(tenant, targetId)); + } + + private boolean isTargetInfoNew(final Object targetInfo) { + return ((TargetInfo) targetInfo).isNew(); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/event/AbstractBaseEntityEvent.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/event/AbstractBaseEntityEvent.java new file mode 100755 index 000000000..7d0973f74 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/event/AbstractBaseEntityEvent.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus.event; + +import org.eclipse.hawkbit.repository.model.BaseEntity; + +/** + * An abstract definition class for {@link EntityEvent} for {@link BaseEntity}s, + * which holds the {@link BaseEntity}. + * + * + * + * @param + * the type of the {@link BaseEntity} + */ +public abstract class AbstractBaseEntityEvent extends AbstractDistributedEvent + implements EntityEvent { + + /** + * + */ + private static final long serialVersionUID = 1L; + private final E entity; + + /** + * @param baseEntity + * the entity which has been created or modified + */ + public AbstractBaseEntityEvent(final E baseEntity) { + super(baseEntity.getOptLockRevision(), baseEntity.getTenant()); + this.entity = baseEntity; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.eventbus.event.EntityEvent#getEntity() + */ + @Override + public E getEntity() { + return entity; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.eventbus.event.EntityEvent#getEntity(java.lang + * .Class) + */ + @Override + public T getEntity(final Class entityClass) { + return entityClass.cast(entity); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.eventbus.event.EntityEvent#getTenant() + */ + @Override + public String getTenant() { + return entity.getTenant(); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/event/TargetAssignDistributionSetEvent.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/event/TargetAssignDistributionSetEvent.java new file mode 100755 index 000000000..8c13800c9 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/event/TargetAssignDistributionSetEvent.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus.event; + +import java.net.URI; +import java.util.Collection; + +import org.eclipse.hawkbit.repository.model.SoftwareModule; + +/** + * Event that gets sent when a distribution set gets assigned to a target. + * + * + * + */ +public class TargetAssignDistributionSetEvent { + + private final Collection softwareModules; + private final String controllerId; + private final Long actionId; + private final URI targetAdress; + + /** + * Creates a new {@link TargetAssignDistributionSetEvent}. + * + * @param controllerId + * the ID of the controller + * @param actionId + * the action id of the assignment + * @param softwareModules + * the software modules which have been assigned to the target + * @param targetAdress + * the targetAdress of the target + */ + public TargetAssignDistributionSetEvent(final String controllerId, final Long actionId, + final Collection softwareModules, final URI targetAdress) { + this.controllerId = controllerId; + this.actionId = actionId; + this.softwareModules = softwareModules; + this.targetAdress = targetAdress; + } + + /** + * @return the action id of the assignment + */ + public Long getActionId() { + return actionId; + } + + /** + * @return the controllerId of the Target which has been assigned to the + * distribution set + */ + public String getControllerId() { + return controllerId; + } + + /** + * @return the software modules which have been assigned to the target + */ + public Collection getSoftwareModules() { + return softwareModules; + } + + public URI getTargetAdress() { + return targetAdress; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/event/TargetCreatedEvent.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/event/TargetCreatedEvent.java new file mode 100755 index 000000000..3e80e45a5 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/event/TargetCreatedEvent.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus.event; + +import org.eclipse.hawkbit.repository.model.Target; + +/** + * Defines the {@link AbstractBaseEntityEvent} of creating a new {@link Target}. + * + * + * + * + */ +public class TargetCreatedEvent extends AbstractBaseEntityEvent { + + private static final long serialVersionUID = 1L; + + /** + * @param target + * the target which has been created + */ + public TargetCreatedEvent(final Target target) { + super(target); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/event/TargetInfoUpdateEvent.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/event/TargetInfoUpdateEvent.java new file mode 100755 index 000000000..1119026bd --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/eventbus/event/TargetInfoUpdateEvent.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus.event; + +import org.eclipse.hawkbit.repository.model.TargetInfo; + +/** + * + * + * + */ +public class TargetInfoUpdateEvent implements EntityEvent { + + private final long revision; + private final TargetInfo targetInfo; + private final String tenant; + private String originNodeId; + private String nodeId; + + /** + * @param targetInfo + * the target info entity + */ + public TargetInfoUpdateEvent(final TargetInfo targetInfo) { + this.targetInfo = targetInfo; + this.tenant = targetInfo.getTarget().getTenant(); + this.revision = -1; + } + + @Override + public void setOriginNodeId(final String originNodeId) { + this.originNodeId = originNodeId; + } + + @Override + public void setNodeId(final String nodeId) { + this.nodeId = nodeId; + + } + + @Override + public String getOriginNodeId() { + return this.originNodeId; + } + + @Override + public String getNodeId() { + return this.nodeId; + } + + @Override + public long getRevision() { + return revision; + } + + @Override + public E getEntity(final Class entityClass) { + return entityClass.cast(targetInfo); + } + + @Override + public TargetInfo getEntity() { + return targetInfo; + } + + @Override + public String getTenant() { + return tenant; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/AbstractReportSeries.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/AbstractReportSeries.java new file mode 100755 index 000000000..0542f68c9 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/AbstractReportSeries.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.report.model; + +import java.io.Serializable; + +/** + * An abstract report series. + * + * + * + * + */ +public class AbstractReportSeries implements Serializable { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final String name; + + /** + * @param name + * the name of the series + */ + public AbstractReportSeries(final String name) { + this.name = name; + } + + /** + * @return the name of the series + */ + public String getName() { + return name; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/DataReportSeries.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/DataReportSeries.java new file mode 100755 index 000000000..97623d9d0 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/DataReportSeries.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.report.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +/** + * A data report series which contains a list of {@link DataReportSeriesItem}. + * + * + * + * @param + * the type of the report series item + */ +public class DataReportSeries extends AbstractReportSeries { + + private final List> data = new ArrayList<>(); + + /** + * @param name + * the name of data series + */ + public DataReportSeries(final String name) { + super(name); + } + + /** + * Constructs a ListSeries with the given series name and array of values. + * + * @param name + * the name of the data series + * @param values + * data report series item for this data series. + */ + public DataReportSeries(final String name, final List> values) { + this(name); + setData(values); + } + + private void setData(final List> values) { + this.data.clear(); + data.addAll(values); + } + + /** + * @return An array of the numeric values + */ + @SuppressWarnings("unchecked") + public DataReportSeriesItem[] getData() { + return data.toArray(new DataReportSeriesItem[data.size()]); + } + + public Stream> getDataStream() { + return data.stream(); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/DataReportSeriesItem.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/DataReportSeriesItem.java new file mode 100755 index 000000000..3369ef1c0 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/DataReportSeriesItem.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.report.model; + +/** + * An data report series item which contains a type and a value. + * + * + * + * @param + * the type of the report series item + */ +public class DataReportSeriesItem { + + private final T type; + private final Number data; + + /** + * @param type + * the type of the data report series item + * @param data + * the data of the report series item + */ + public DataReportSeriesItem(final T type, final Number data) { + this.type = type; + this.data = data; + } + + /** + * @return the type of the data report item + */ + public T getType() { + return type; + } + + /** + * @return the data of the data report item + */ + public Number getData() { + return data; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/InnerOuterDataReportSeries.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/InnerOuterDataReportSeries.java new file mode 100755 index 000000000..1568847de --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/InnerOuterDataReportSeries.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.report.model; + +/** + * A double data series which contains an inner and an outer series ideal for + * showing donut charts. + * + * + * + * @param + * The type parameter for the report series data + */ +public class InnerOuterDataReportSeries { + + private final DataReportSeries innerSeries; + private final DataReportSeries outerSeries; + + /** + * @param innerSeries + * the innerseries of an circle donut chart + * @param outerSeries + * the outer series of an donut chart + */ + public InnerOuterDataReportSeries(final DataReportSeries innerSeries, final DataReportSeries outerSeries) { + this.innerSeries = innerSeries; + this.outerSeries = outerSeries; + } + + /** + * @return the innerSeries + */ + public DataReportSeries getInnerSeries() { + return innerSeries; + } + + /** + * @return the outerSeries + */ + public DataReportSeries getOuterSeries() { + return outerSeries; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/ListReportSeries.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/ListReportSeries.java new file mode 100755 index 000000000..55125e046 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/ListReportSeries.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.report.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A simple list report series which just contains a list of values of a report. + * + * + * + * + */ +public class ListReportSeries extends AbstractReportSeries { + + private final List data = new ArrayList(); + + /** + * @param name + * the name of the list report series + */ + public ListReportSeries(final String name) { + super(name); + } + + /** + * Constructs a ListSeries with the given series name and array of values. + * + * @param name + * the name of the list report series + * @param values + * the values of the list report series + */ + public ListReportSeries(final String name, final Number... values) { + this(name); + setData(values); + } + + /** + * Sets the values in the list series to the ones provided. + * + * @param values + */ + private void setData(final Number... values) { + this.data.clear(); + Collections.addAll(this.data, values); + } + + /** + * @return An array of the numeric values + */ + public Number[] getData() { + return data.toArray(new Number[data.size()]); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/SeriesTime.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/SeriesTime.java new file mode 100755 index 000000000..a50171479 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/report/model/SeriesTime.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.report.model; + +/** + * A series time enum definition for {@link DataReportSeriesItem}s. + * + * + * + * + */ +public enum SeriesTime { + + /** + * hour. + */ + HOUR, + /** + * day. + */ + DAY, + /** + * week. + */ + WEEK, + /** + * month. + */ + MONTH, + /** + * year. + */ + YEAR, + + /** + * more than one year. + */ + MORE_THAN_YEAR, + + /** + * never. + */ + NEVER; + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ActionRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ActionRepository.java new file mode 100755 index 000000000..a97799ab4 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ActionRepository.java @@ -0,0 +1,240 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.Collection; +import java.util.List; + +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.Target; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; + +/** + * {@link Action} repository. + * + * + * + * + * + */ +@Transactional(readOnly = true) +public interface ActionRepository extends BaseEntityRepository, JpaSpecificationExecutor { + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.CrudRepository#findAll() + */ + @Override + @EntityGraph(value = "Action.all", type = EntityGraphType.LOAD) + Iterable findAll(); + + /** + * Retrieves an Action with all lazy attributes. + * + * @param actionId + * the ID of the action + * @return the found {@link Action} + */ + @EntityGraph(value = "Action.all", type = EntityGraphType.LOAD) + Action findById(Long actionId); + + /** + * Retrieves all {@link Action}s which are referring the given + * {@link DistributionSet}. + * + * @param pageable + * page parameters + * @param ds + * the {@link DistributionSet} on which will be filtered + * @return the found {@link Action}s + */ + @EntityGraph(value = "Action.all", type = EntityGraphType.LOAD) + Page findByDistributionSet(final Pageable pageable, final DistributionSet ds); + + /** + * Retrieves all {@link Action}s which are referring the given + * {@link Target}. + * + * @param pageable + * page parameters + * @param target + * the target to find assigned actions + * @return the found {@link Action}s + */ + Slice findByTarget(Pageable pageable, Target target); + + /** + * Retrieves all {@link Action}s which are active and referring the given + * {@link Target} in a specified order. + * + * @param pageable + * page parameters + * @param target + * the target to find assigned actions + * @param active + * the action active flag + * @return the found {@link Action}s + */ + @EntityGraph(value = "Action.ds", type = EntityGraphType.LOAD) + List findByTargetAndActiveOrderByIdAsc(final Target target, boolean active); + + /** + * Retrieves latest {@link UpdateAction} for given target and + * {@link SoftwareModule}. + * + * @param targetId + * to search for + * @param module + * to search for + * @return action if there is one with assigned target and module is part of + * assigned {@link DistributionSet}. + */ + @Query("Select a from Action a join a.distributionSet ds join ds.modules modul where a.target.controllerId = :target and modul = :module order by a.id desc") + List findActionByTargetAndSoftwareModule(@Param("target") final String targetId, + @Param("module") SoftwareModule module); + + /** + * Retrieves all {@link UpdateAction}s which are referring the given + * {@link DistributionSet} and {@link Target}. + * + * @param pageable + * page parameters + * @param target + * is the assigned target + * @param ds + * the {@link DistributionSet} on which will be filtered + * @return the found {@link UpdateAction}s + */ + @Query("Select a from Action a where a.target = :target and a.distributionSet = :ds order by a.id") + @EntityGraph(value = "Action.all", type = EntityGraphType.LOAD) + Page findByTargetAndDistributionSet(final Pageable pageable, @Param("target") final Target target, + @Param("ds") DistributionSet ds); + + /** + * Retrieves all {@link Action}s of a specific target, without pagination + * ordered by action ID. + * + * @param target + * to search for + * @return a list of actions according to the searched target + */ + @Query("Select a from Action a where a.target = :target order by a.id") + List findByTarget(@Param("target") final Target target); + + /** + * Retrieves all {@link Action}s of a specific target and given active flag + * ordered by action ID. + * + * @param pageable + * the pagination parameter + * @param target + * to search for + * @param active + * {@code true} for all actions which are currently active, + * {@code false} for inactive + * @return a paged list of actions ordered by action ID + */ + @Query("Select a from Action a where a.target = :target and a.active= :active order by a.id") + Page findByActiveAndTarget(Pageable pageable, @Param("target") Target target, + @Param("active") boolean active); + + /** + * Retrieves all {@link Action}s of a specific target and given active flag + * ordered by action ID. + * + * @param target + * to search for + * @param active + * {@code true} for all actions which are currently active, + * {@code false} for inactive + * @return a list of actions ordered by action ID + */ + @EntityGraph(value = "Action.ds", type = EntityGraphType.LOAD) + @Query("Select a from Action a where a.target = :target and a.active= :active order by a.id") + List findByActiveAndTarget(@Param("target") Target target, @Param("active") boolean active); + + /** + * Updates all {@link Action} to inactive for all targets with given ID. + * + * @param keySet + * the list of actions to set inactive + * @param targetsIds + * the IDs of the targets according to the action to set in + * active + */ + @Modifying + @Transactional + @Query("UPDATE Action a SET a.active = false WHERE a IN :keySet AND a.target IN :targetsIds") + void setToInactive(@Param("keySet") List keySet, @Param("targetsIds") List targetsIds); + + /** + * Retrieves all {@link Action}s which are active and referring to the given + * target Ids and distribution set required migration step. + * + * @param targetIds + * the IDs of targets for the actions + * @param notStatus + * the status which the actions should not have + * @return the found list of {@link Action}s + */ + @Query("SELECT a FROM Action a WHERE a.active = true AND a.distributionSet.requiredMigrationStep = false AND a.target IN ?1 AND a.status != ?2") + List findByActiveAndTargetIdInAndActionStatusNotEqualToAndDistributionSetRequiredMigrationStep( + Collection targetIds, Action.Status notStatus); + + /** + * Counts all {@link Action}s referring to the given target. + * + * @param target + * the target to count the {@link Action}s + * @return the count of actions referring to the given target + */ + Long countByTarget(Target target); + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.CrudRepository#save(java.lang. + * Iterable) + */ + @Override + @CacheEvict(value = "feedbackReceivedOverTime", allEntries = true) + List save(Iterable entities); + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.CrudRepository#save(java.lang.Object) + */ + @Override + @CacheEvict(value = "feedbackReceivedOverTime", allEntries = true) + S save(S entity); + + /** + * Counts all {@link Action}s referring to the given DistributionSet. + * + * @param distributionSet + * DistributionSet to count the {@link Action}s from + * @return the count of actions referring to the given target + */ + Long countByDistributionSet(DistributionSet distributionSet); +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ActionStatusRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ActionStatusRepository.java new file mode 100755 index 000000000..5e2800755 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ActionStatusRepository.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.transaction.annotation.Transactional; + +/** + * {@link ActionStatus} repository. + * + * + * + * + */ +@Transactional(readOnly = true) +public interface ActionStatusRepository + extends BaseEntityRepository, JpaSpecificationExecutor { + + /** + * @param target + * @param action + * @return + */ + Long countByAction(Action action); + + /** + * @param action + * @param retrieved + * @return + */ + Long countByActionAndStatus(Action action, Status retrieved); + + /** + * @param pageReq + * @param action + * @return + */ + Page findByAction(Pageable pageReq, Action action); + + /** + * @param pageReq + * @param action + * @return + */ + Page findByActionOrderByIdDesc(Pageable pageReq, Action action); + + /** + * Finds all status updates for the defined action and target order by + * {@link ActionStatus#getId()} desc including + * {@link ActionStatus#getMessages()}. + * + * @param pageReq + * for page configuration + * @param target + * to look for + * @param action + * to look for + * @return Page with found targets + */ + @EntityGraph(value = "ActionStatus.withMessages", type = EntityGraphType.LOAD) + Page getByActionOrderByIdDesc(Pageable pageReq, Action action); + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ArtifactManagement.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ArtifactManagement.java new file mode 100755 index 000000000..a5ee019dc --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ArtifactManagement.java @@ -0,0 +1,501 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.io.InputStream; +import java.util.List; + +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.artifact.repository.ArtifactRepository; +import org.eclipse.hawkbit.artifact.repository.ArtifactStoreException; +import org.eclipse.hawkbit.artifact.repository.HashNotMatchException; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash; +import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.eclipse.hawkbit.repository.exception.ArtifactDeleteFailedException; +import org.eclipse.hawkbit.repository.exception.ArtifactUploadFailedException; +import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.exception.GridFSDBFileNotFoundException; +import org.eclipse.hawkbit.repository.exception.InvalidMD5HashException; +import org.eclipse.hawkbit.repository.exception.InvalidSHA1HashException; +import org.eclipse.hawkbit.repository.model.Artifact; +import org.eclipse.hawkbit.repository.model.ExternalArtifact; +import org.eclipse.hawkbit.repository.model.ExternalArtifactProvider; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.specifications.SoftwareModuleSpecification; +import org.hibernate.validator.constraints.NotEmpty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.hateoas.Identifiable; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * service for {@link Artifact} management operations. + * + * + * + * + */ +@Transactional(readOnly = true) +@Validated +@Service +public class ArtifactManagement { + + private static final Logger LOG = LoggerFactory.getLogger(ArtifactManagement.class); + + @Autowired + private LocalArtifactRepository localArtifactRepository; + + @Autowired + private ExternalArtifactRepository externalArtifactRepository; + + @Autowired + private SoftwareModuleRepository softwareModuleRepository; + + @Autowired + private ExternalArtifactProviderRepository externalArtifactProviderRepository; + + @Autowired + private ArtifactRepository artifactRepository; + + /** + * Persists artifact binary as provided by given InputStream. assign the + * artifact in addition to given {@link SoftwareModule}. + * + * @param stream + * to read from for artifact binary + * @param moduleId + * to assign the new artifact to + * @param filename + * of the artifact + * @param providedSha1Sum + * optional sha1 checksum to check the new file against + * @param providedMd5Sum + * optional md5 checksum to check the new file against + * @param overrideExisting + * to true if the artifact binary can be overdiden + * if it already exists + * @param contentType + * the contentType of the file + * @return uploaded {@link LocalArtifact} + * + * @throws EntityNotFoundException + * if given software module does not exist + * @throws EntityAlreadyExistsException + * if File with that name already exists in the Software Module + * @throws ArtifactUploadFailedException + * if upload fails with internal server errors + * @throws InvalidMD5HashException + * if check against provided MD5 checksum failed + * @throws InvalidSHA1HashException + * if check against provided SHA1 checksum failed + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_REPOSITORY) + public LocalArtifact createLocalArtifact(@NotNull final InputStream stream, @NotNull final Long moduleId, + @NotEmpty final String filename, final String providedMd5Sum, final String providedSha1Sum, + final boolean overrideExisting, final String contentType) { + DbArtifact result = null; + + final SoftwareModule softwareModule = getModuleAndThrowExceptionIfThatFails(moduleId); + + final LocalArtifact existing = checkForExistingArtifact(filename, overrideExisting, softwareModule); + + try { + result = artifactRepository.store(stream, filename, contentType, + new DbArtifactHash(providedSha1Sum, providedMd5Sum)); + } catch (final ArtifactStoreException e) { + throw new ArtifactUploadFailedException(e); + } catch (final HashNotMatchException e) { + if (e.getHashFunction().equals(HashNotMatchException.SHA1)) { + throw new InvalidSHA1HashException(e.getMessage(), e); + } else { + throw new InvalidMD5HashException(e.getMessage(), e); + } + } + if (result == null) { + return null; + } + + return storeArtifactMetadata(softwareModule, filename, result, existing); + } + + private LocalArtifact checkForExistingArtifact(final String filename, final boolean overrideExisting, + final SoftwareModule softwareModule) { + if (softwareModule.getLocalArtifactByFilename(filename).isPresent()) { + if (overrideExisting) { + LOG.debug("overriding existing artifact with new filename {}", filename); + return softwareModule.getLocalArtifactByFilename(filename).get(); + } else { + throw new EntityAlreadyExistsException("File with that name already exists in the Software Module"); + } + } + return null; + } + + private SoftwareModule getModuleAndThrowExceptionIfThatFails(final Long moduleId) { + final SoftwareModule softwareModule = findSoftwareModuleWithDetails(moduleId); + + if (softwareModule == null) { + LOG.debug("no software module with ID {} exists", moduleId); + throw new EntityNotFoundException("Software Module: " + moduleId); + } + return softwareModule; + } + + /** + * Retrieves software module including details ( + * {@link SoftwareModule#getArtifacts()}). + * + * @param id + * parameter + * @param isDeleted + * parameter + * @return the found {@link SoftwareModule}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + protected SoftwareModule findSoftwareModuleWithDetails(@NotNull final Long id) { + final SoftwareModule result = findSoftwareModuleById(id); + if (result != null) { + result.getArtifacts().size(); + } + + return result; + } + + /** + * Find all local artifact by sha1 and return the first artifact. + * + * @param sha1 + * the sha1 + * @return the first local artifact + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY + SpringEvalExpressions.HAS_AUTH_OR + + SpringEvalExpressions.IS_CONTROLLER) + public LocalArtifact findFirstLocalArtifactsBySHA1(final String sha1) { + return localArtifactRepository.findFirstByGridFsFileName(sha1); + } + + /** + * Finds {@link SoftwareModule} by given id. + * + * @param id + * to search for + * @return the found {@link SoftwareModule}s or null if not + * found. + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY + SpringEvalExpressions.HAS_AUTH_OR + + SpringEvalExpressions.IS_CONTROLLER) + protected SoftwareModule findSoftwareModuleById(@NotNull final Long id) { + + final Specification spec = SoftwareModuleSpecification.byId(id); + + return softwareModuleRepository.findOne(spec); + } + + private LocalArtifact storeArtifactMetadata(final SoftwareModule softwareModule, final String providedFilename, + final DbArtifact result, final LocalArtifact existing) { + LocalArtifact artifact = existing; + if (existing == null) { + artifact = new LocalArtifact(result.getHashes().getSha1(), providedFilename, softwareModule); + } + artifact.setMd5Hash(result.getHashes().getMd5()); + artifact.setSha1Hash(result.getHashes().getSha1()); + artifact.setSize(result.getSize()); + + LOG.debug("storing new artifact into repository {}", artifact); + final LocalArtifact artifactPersisted = localArtifactRepository.save(artifact); + + return artifactPersisted; + } + + /** + * Persists {@link ExternalArtifactProvider} based on given properties. + * + * @param name + * of the provided + * @param description + * which is optional + * @param basePath + * of all {@link ExternalArtifact}s of the provider + * @param defaultUrlSuffix + * that is used if {@link ExternalArtifact#getUrlSuffix()} is + * empty. + * @return created {@link ExternalArtifactProvider} + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_REPOSITORY) + public ExternalArtifactProvider createExternalArtifactProvider(@NotEmpty final String name, + final String description, @NotNull final String basePath, final String defaultUrlSuffix) { + return externalArtifactProviderRepository + .save(new ExternalArtifactProvider(name, description, basePath, defaultUrlSuffix)); + } + + /** + * Creates {@link ExternalArtifact} based on given provider. + * + * @param externalRepository + * the artifact is located in + * @param urlSuffix + * of the artifact + * {@link ExternalArtifactProvider#getDefaultSuffix()} is used if + * empty. + * @param moduleId + * to assign the artifact to + * + * @return created {@link ExternalArtifact} + * + * @throws EntityNotFoundException + * if {@link SoftwareModule} with given ID does not exist + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_REPOSITORY) + public ExternalArtifact createExternalArtifact(@NotNull final ExternalArtifactProvider externalRepository, + final String urlSuffix, @NotNull final Long moduleId) { + + final SoftwareModule module = getModuleAndThrowExceptionIfThatFails(moduleId); + final ExternalArtifact result = externalArtifactRepository + .save(new ExternalArtifact(externalRepository, urlSuffix, module)); + + return result; + } + + /** + * Deletes {@link Artifact} based on given id. + * + * @param id + * of the {@link Artifact} that has to be deleted. + * @throws ArtifactDeleteFailedException + * if deletion failed (MongoDB is not available) + * + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_DELETE_REPOSITORY) + public void deleteLocalArtifact(@NotNull final Long id) { + final LocalArtifact existing = localArtifactRepository.findOne(id); + + if (null == existing) { + return; + } + + deleteGridFsArtifact(existing); + + existing.getSoftwareModule().removeArtifact(existing); + softwareModuleRepository.save(existing.getSoftwareModule()); + localArtifactRepository.delete(id); + } + + /** + * Delete a grid fs file. + * + * @param existing + * the related local artifact + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_DELETE_REPOSITORY) + public void deleteGridFsArtifact(@NotNull final LocalArtifact existing) { + if (existing == null) { + return; + } + + boolean artifactIsOnlyUsedByOneSoftwareModule = true; + for (LocalArtifact lArtifact : localArtifactRepository.findByGridFsFileName(existing.getGridFsFileName())) { + if (!lArtifact.getSoftwareModule().isDeleted() + && lArtifact.getSoftwareModule().getId() != existing.getSoftwareModule().getId()) { + artifactIsOnlyUsedByOneSoftwareModule = false; + break; + } + } + + if (artifactIsOnlyUsedByOneSoftwareModule) { + try { + LOG.debug("deleting artifact from repository {}", existing.getGridFsFileName()); + artifactRepository.deleteBySha1(existing.getGridFsFileName()); + } catch (final ArtifactStoreException e) { + throw new ArtifactDeleteFailedException(e); + } + } + } + + /** + * Deletes {@link Artifact} based on given id. + * + * @param id + * of the {@link Artifact} that has to be deleted. + * @throws ArtifactDeleteFailedException + * if deletion failed (MongoDB is not available) + * + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_DELETE_REPOSITORY) + public void deleteExternalArtifact(@NotNull final Long id) { + final ExternalArtifact existing = externalArtifactRepository.findOne(id); + + if (null == existing) { + return; + } + + existing.getSoftwareModule().removeArtifact(existing); + softwareModuleRepository.save(existing.getSoftwareModule()); + externalArtifactRepository.delete(id); + } + + /** + * Searches for {@link Artifact} with given file name. + * + * @param filename + * to search for + * @return found List of {@link LocalArtifact}s. + */ + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY + SpringEvalExpressions.HAS_AUTH_OR + + SpringEvalExpressions.IS_CONTROLLER) + public List findLocalArtifactByFilename(@NotNull final String filename) { + return localArtifactRepository.findByFilename(filename); + } + + /** + * Searches for {@link Artifact} with given {@link Identifiable}. + * + * @param id + * to search for + * @return found {@link Artifact} or null is it could not be + * found. + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Artifact findArtifact(@NotNull final Long id) { + return localArtifactRepository.findOne(id); + } + + /** + * Loads {@link org.eclipse.hawkbit.artifact.server.json.model.Artifact} + * from store for given {@link LocalArtifact}. + * + * @param artifact + * to search for + * @return loaded + * {@link org.eclipse.hawkbit.artifact.server.json.model.Artifact} + * + * @throws GridFSDBFileNotFoundException + * if file could not be found in store + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_DOWNLOAD_ARTIFACT + SpringEvalExpressions.HAS_AUTH_OR + + SpringEvalExpressions.IS_CONTROLLER) + public DbArtifact loadLocalArtifactBinary(@NotNull final LocalArtifact artifact) { + final DbArtifact result = artifactRepository.getArtifactBySha1(artifact.getGridFsFileName()); + if (result == null) { + throw new GridFSDBFileNotFoundException(artifact.getGridFsFileName()); + } + + return result; + } + + /** + * Persists artifact binary as provided by given InputStream. assign the + * artifact in addition to given {@link SoftwareModule}. + * + * @param inputStream + * to read from for artifact binary + * @param moduleId + * to assign the new artifact to + * @param filename + * of the artifact + * @param overrideExisting + * to true if the artifact binary can be overdiden + * if it already exists + * @param contentType + * the contentType of the file + * + * @return uploaded {@link LocalArtifact} + * + * @throw ArtifactUploadFailedException if upload failes + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public LocalArtifact createLocalArtifact(final InputStream inputStream, final Long moduleId, final String filename, + final boolean overrideExisting, final String contentType) { + return createLocalArtifact(inputStream, moduleId, filename, null, null, overrideExisting, contentType); + } + + /** + * Persists artifact binary as provided by given InputStream. assign the + * artifact in addition to given {@link SoftwareModule}. + * + * @param inputStream + * to read from for artifact binary + * @param moduleId + * to assign the new artifact to + * @param filename + * of the artifact + * @param overrideExisting + * to true if the artifact binary can be overdiden + * if it already exists + * + * @return uploaded {@link LocalArtifact} + * + * @throw ArtifactUploadFailedException if upload failes + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public LocalArtifact createLocalArtifact(final InputStream inputStream, final Long moduleId, final String filename, + final boolean overrideExisting) { + return createLocalArtifact(inputStream, moduleId, filename, null, null, overrideExisting, null); + } + + /** + * Get local artifact for a base software module. + * + * @param pageReq + * Pageable + * @param swId + * software module id + * @return Page + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Page findLocalArtifactBySoftwareModule(@NotNull final Pageable pageReq, + @NotNull final Long swId) { + return localArtifactRepository.findBySoftwareModuleId(pageReq, swId); + } + + /** + * Find by artifact by software module id and filename. + * + * @param filename + * file name + * @param softwareModuleId + * software module id. + * @return LocalArtifact if artifact present + */ + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY + SpringEvalExpressions.HAS_AUTH_OR + + SpringEvalExpressions.IS_CONTROLLER) + public List findByFilenameAndSoftwareModule(@NotNull final String filename, + @NotNull final Long softwareModuleId) { + return localArtifactRepository.findByFilenameAndSoftwareModuleId(filename, softwareModuleId); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/AssignmentResult.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/AssignmentResult.java new file mode 100755 index 000000000..98e1320b7 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/AssignmentResult.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +/** + * Generic assigment result bean. + * + * + * + * + */ +public class AssignmentResult { + + private final int total; + private final int assigned; + private final int alreadyAssigned; + + /** + * Constructor. + * + * @param assigned + * is the number of newly assigned elements. + * @param alreadyAssigned + * is the number of already assigned elements. + */ + public AssignmentResult(final int assigned, final int alreadyAssigned) { + super(); + this.assigned = assigned; + this.alreadyAssigned = alreadyAssigned; + total = assigned + alreadyAssigned; + } + + /** + * @return the assignedTargets + */ + public int getAssigned() { + return assigned; + } + + /** + * @return the total + */ + public int getTotal() { + return total; + } + + /** + * @return the alreadyAssigned + */ + public int getAlreadyAssigned() { + return alreadyAssigned; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/BaseEntityRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/BaseEntityRepository.java new file mode 100755 index 000000000..539d20f71 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/BaseEntityRepository.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.io.Serializable; + +import org.eclipse.hawkbit.repository.model.BaseEntity; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.transaction.annotation.Transactional; + +/** + * Command repository operations for all {@link BaseEntity}s. + * + * + * + * + * @param + * type if the entity type + * @param + * of the entity type + */ +@NoRepositoryBean +@Transactional(readOnly = true) +public interface BaseEntityRepository + extends PagingAndSortingRepository { + + /** + * Deletes all {@link BaseEntity} of a given tenant. + * + * @param tenant + * to delete data from + */ + @Modifying + @Transactional + void deleteByTenantIgnoreCase(String tenant); + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java new file mode 100755 index 000000000..55ed458ec --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java @@ -0,0 +1,588 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.persistence.EntityManager; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.exception.ToManyAttributeEntriesException; +import org.eclipse.hawkbit.repository.exception.ToManyStatusEntriesException; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.ActionStatus_; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetInfo; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.eclipse.hawkbit.repository.model.Target_; +import org.hibernate.validator.constraints.NotEmpty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * Service layer for all operations of the controller API (with access + * permissions only for the controller). + * + * + * + */ +@Transactional(readOnly = true) +@Validated +@Service +public class ControllerManagement implements EnvironmentAware { + private static final Logger LOG = LoggerFactory.getLogger(ControllerManagement.class); + private static final Logger LOG_DOS = LoggerFactory.getLogger("server-security.dos"); + + @Autowired + private EntityManager entityManager; + + @Autowired + private ActionRepository actionRepository; + + @Autowired + private TargetRepository targetRepository; + + @Autowired + private TargetManagement targetManagement; + + @Autowired + private DeploymentManagement deploymentManagement; + + @Autowired + private TargetInfoRepository targetInfoRepository; + + @Autowired + private SoftwareModuleRepository softwareModuleRepository; + + @Autowired + private ActionStatusRepository actionStatusRepository; + + private Integer maxCount = 1000; + + private Integer maxAttributes = 100; + + /** + * Refreshes the time of the last time the controller has been connected to + * the server. + * + * @param targetid + * of the target to to update + * @param address + * the client address of the target, might be {@code null} + * @return the updated target + * + * @throws EntityNotFoundException + * if target with given ID could not be found + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + public Target updateLastTargetQuery(@NotEmpty final String targetid, final URI address) { + final Target target = targetRepository.findByControllerId(targetid); + if (target == null) { + throw new EntityNotFoundException(targetid); + } + + return updateLastTargetQuery(target.getTargetInfo(), address).getTarget(); + } + + /** + * Retrieves last {@link UpdateAction} for a download of an artifact of + * given module and target. + * + * @param targetId + * to look for + * @param module + * that should be assigned to the target + * @return last {@link UpdateAction} for given combination + * + * @throws EntityNotFoundException + * if action for given combination could not be found + */ + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + public Action getActionForDownloadByTargetAndSoftwareModule(@NotNull final String targetId, + @NotNull final SoftwareModule module) { + final List action = actionRepository.findActionByTargetAndSoftwareModule(targetId, module); + + if (action.isEmpty() || action.get(0).isCancelingOrCanceled()) { + throw new EntityNotFoundException("No assigment found for module " + module.getId() + " to target " + + targetId); + } + + return action.get(0); + } + + /** + * Refreshes the time of the last time the controller has been connected to + * the server. + * + * @param target + * to update + * @param address + * the client address of the target, might be {@code null} + * @return the updated target + * + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + public TargetInfo updateLastTargetQuery(@NotNull final TargetInfo target, final URI address) { + return updateTargetStatus(target, null, System.currentTimeMillis(), address); + } + + /** + * Retrieves all {@link Action}s which are active and assigned to a + * {@link Target}. + * + * @param target + * the target to retrieve the actions from + * @return a list of actions assigned to given target which are active + */ + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + public List findActionByTargetAndActive(final Target target) { + return actionRepository.findByTargetAndActiveOrderByIdAsc(target, true); + } + + /** + * Retrieves all {@link SoftwareModule}s which are assigned to the given + * {@link DistributionSet}. + * + * @param distributionSet + * the distribution set which should be assigned to the returned + * {@link SoftwareModule}s + * @return a list of {@link SoftwareModule}s assigned to given + * {@code distributionSet} + */ + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + public List findSoftwareModulesByDistributionSet(final DistributionSet distributionSet) { + return softwareModuleRepository.findByAssignedTo(distributionSet); + } + + /** + * Get the {@link Action} entity for given actionId with all lazy + * attributes. + * + * @param actionId + * to be id of the action + * @return the corresponding {@link Action} + */ + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + public Action findActionWithDetails(@NotNull final Long actionId) { + return actionRepository.findById(actionId); + } + + /** + * register new target in the repository (plug-and-play). + * + * @param targetid + * reference + * @param address + * the client IP address of the target, might be {@code null} + * @return target reference + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + public Target findOrRegisterTargetIfItDoesNotexist(@NotNull final String targetid, final URI address) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + return cb.equal(targetRoot.get(Target_.controllerId), targetid); + } + }; + + Target target = targetRepository.findOne(spec); + + if (target == null) { + target = new Target(targetid); + target.setDescription("Plug and Play target: " + targetid); + target.setName(targetid); + return targetManagement.createTarget(target, TargetUpdateStatus.REGISTERED, System.currentTimeMillis(), + address); + } else { + return updateLastTargetQuery(target.getTargetInfo(), address).getTarget(); + } + } + + /** + * Update selective the target status of a given {@code target}. + * + * @param targetInfo + * the target to update the target status + * @param status + * the status to be set of the target. Might be {@code null} if + * the target status should not be updated + * @param lastTargetQuery + * the last target query to be set of the target. Might be + * {@code null} if the target lastTargetQuery should not be + * updated + * @param address + * the client address of the target, might be {@code null} + * @return the updated TargetInfo + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + public TargetInfo updateTargetStatus(@NotNull final TargetInfo targetInfo, final TargetUpdateStatus status, + final Long lastTargetQuery, final URI address) { + final TargetInfo mtargetInfo = entityManager.merge(targetInfo); + if (status != null) { + mtargetInfo.setUpdateStatus(status); + } + if (lastTargetQuery != null) { + mtargetInfo.setLastTargetQuery(lastTargetQuery); + } + if (address != null) { + mtargetInfo.setAddress(address.toString()); + } + return targetInfoRepository.save(mtargetInfo); + } + + /** + * Reports an {@link ActionStatus} for a {@link CancelAction}. + * + * @param actionStatusMessages + * to be updated + * @param target2 + * + * @throws EntityAlreadyExistsException + * if a given entity already exists + * @throws ToManyStatusEntriesException + * if more than the allowed number of status entries are + * inserted + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + public void addCancelActionStatus(@NotNull final ActionStatus actionStatus, final Action action) { + + checkForToManyStatusEntries(action); + action.setStatus(actionStatus.getStatus()); + + switch (actionStatus.getStatus()) { + case WARNING: + case ERROR: + case RUNNING: + break; + case CANCELED: + case FINISHED: + // in case of successful cancelation we also report the success at + // the canceled action + // itself. + actionStatus.addMessage("Cancelation completion is finished sucessfully."); + // set action inactive + action.setActive(false); + successCancellation(action); + break; + case RETRIEVED: + actionStatus.addMessage("Cancelation request retrieved"); + break; + default: + } + actionRepository.save(action); + actionStatusRepository.save(actionStatus); + } + + private void successCancellation(final Action action) { + final Target target = action.getTarget(); + final List nextActiveActions = actionRepository.findByTargetAndActiveOrderByIdAsc(target, true) + .stream().filter(a -> !a.getId().equals(action.getId())).collect(Collectors.toList()); + + if (nextActiveActions.isEmpty()) { + target.setAssignedDistributionSet(target.getTargetInfo().getInstalledDistributionSet()); + deploymentManagement.updateTargetInfo(target, TargetUpdateStatus.IN_SYNC, false); + } else { + target.setAssignedDistributionSet(nextActiveActions.get(0).getDistributionSet()); + } + targetManagement.updateTarget(target); + } + + /** + * Reports an {@link ActionStatus} for a {@link UpdateAction}. + * + * @param actionStatus + * to be updated + * @param action + * the update is for + * @return the persisted {@link ActionStatus} + * + * @throws EntityAlreadyExistsException + * if a given entity already exists + * @throws ToManyStatusEntriesException + * if more than the allowed number of status entries are + * inserted + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + public Action addUpdateActionStatus(@NotNull final ActionStatus actionStatus, final Action action) { + + if (!action.isActive()) { + LOG.debug("Update of actionStatus {} for action {} not possible since action not active anymore.", + actionStatus.getId(), action.getId()); + return action; + } + return handleAddUpdateActionStatus(actionStatus, action); + } + + /** + * Sets {@link TargetUpdateStatus} based on given {@link ActionStatus}. + * + * @param actionStatus + * @param action + * @return + */ + public Action handleAddUpdateActionStatus(final ActionStatus actionStatus, final Action action) { + LOG.debug("addUpdateActionStatus for action {}", action.getId()); + + final Action mergedAction = entityManager.merge(action); + Target mergedTarget = mergedAction.getTarget(); + // check for a potential DOS attack + checkForToManyStatusEntries(action); + + switch (actionStatus.getStatus()) { + case ERROR: + mergedTarget = deploymentManagement.updateTargetInfo(mergedTarget, TargetUpdateStatus.ERROR, false); + // set action inactive + mergedAction.setActive(false); + mergedAction.setStatus(Status.ERROR); + mergedTarget.setAssignedDistributionSet(null); + targetManagement.updateTarget(mergedTarget); + break; + case FINISHED: + // set action inactive + mergedAction.setActive(false); + mergedAction.setStatus(Status.FINISHED); + handleFinishedAndStoreInTargetStatus(mergedTarget, mergedAction); + break; + case CANCELED: + case WARNING: + case RUNNING: + deploymentManagement.updateTargetInfo(mergedTarget, TargetUpdateStatus.PENDING, false); + break; + default: + break; + } + + actionStatusRepository.save(actionStatus); + + LOG.debug("addUpdateActionStatus {} for target {} is finished.", action.getId(), mergedTarget.getId()); + + return actionRepository.save(mergedAction); + } + + private void checkForToManyStatusEntries(final Action action) { + if (maxCount > 0) { + + final Long statusCount = actionStatusRepository.countByAction(action); + + if (statusCount >= maxCount) { + LOG_DOS.error( + "Potential denial of service (DOS) attack identfied. More status entries in the system than permitted ({})!", + maxCount); + throw new ToManyStatusEntriesException(String.valueOf(maxCount)); + } + } + } + + private void handleFinishedAndStoreInTargetStatus(final Target target, final Action action) { + final TargetInfo targetInfo = target.getTargetInfo(); + final DistributionSet ds = entityManager.merge(action.getDistributionSet()); + targetInfo.setInstalledDistributionSet(ds); + if (target.getAssignedDistributionSet() != null && targetInfo.getInstalledDistributionSet() != null + && target.getAssignedDistributionSet().getId().equals(targetInfo.getInstalledDistributionSet().getId())) { + targetInfo.setUpdateStatus(TargetUpdateStatus.IN_SYNC); + targetInfo.setInstallationDate(System.currentTimeMillis()); + } else { + targetInfo.setUpdateStatus(TargetUpdateStatus.PENDING); + targetInfo.setInstallationDate(System.currentTimeMillis()); + } + targetInfoRepository.save(targetInfo); + entityManager.detach(ds); + } + + /** + * Updates attributes of the controller. + * + * @param targetid + * to update + * @param data + * to insert + * + * @return updated {@link Target} + * + * @throws EntityNotFoundException + * if target that has to be updated could not be found + * @throws ToManyAttributeEntriesException + * if maximum + */ + @Modifying + @NotNull + @Transactional + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + public Target updateControllerAttributes(@NotEmpty final String targetid, @NotNull final Map data) { + final Target target = targetRepository.findByControllerId(targetid); + + if (target == null) { + throw new EntityNotFoundException(targetid); + } + + target.getTargetInfo().getControllerAttributes().putAll(data); + + if (target.getTargetInfo().getControllerAttributes().size() > maxAttributes) { + LOG_DOS.info( + "Target tries to insert more than the allowed number of entries ({}). DOS attack anticipated!", + maxAttributes); + throw new ToManyAttributeEntriesException(String.valueOf(maxAttributes)); + } + + target.getTargetInfo().setLastTargetQuery(System.currentTimeMillis()); + target.getTargetInfo().setRequestControllerAttributes(false); + return targetRepository.save(target); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.context.EnvironmentAware#setEnvironment(org. + * springframework.core.env. Environment) + */ + @Override + public void setEnvironment(final Environment environment) { + final RelaxedPropertyResolver env = new RelaxedPropertyResolver(environment, "hawkbit.server."); + maxCount = env.getProperty("security.dos.maxStatusEntriesPerAction", Integer.class, 1000); + maxAttributes = env.getProperty("security.dos.maxAttributeEntriesPerTarget", Integer.class, 100); + } + + /** + * Registers retrieved status for given {@link Target} and {@link Action} if + * it does not exist yet. + * + * @param action + * to the handle status for + * @param target + * to the handle status for + * @param message + * for the status + * @return the update action in case the status has been changed to + * {@link Status#RETRIEVED} + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + public Action registerRetrieved(final Action action, final String message) { + return handleRegisterRetrieved(action, message); + } + + /** + * Registers retrieved status for given {@link Target} and {@link Action} if + * it does not exist yet. + * + * @param action + * to the handle status for + * @param message + * for the status + * @return the updated action in case the status has been changed to + * {@link Status#RETRIEVED} + */ + public Action handleRegisterRetrieved(final Action action, final String message) { + // do a manual query with CriteriaBuilder to avoid unnecessary field + // queries and an extra + // count query made by spring-data when using pageable requests, we + // don't need an extra count + // query, we just want to check if the last action status is a retrieved + // or not. + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + final CriteriaQuery queryActionStatus = cb.createQuery(Object[].class); + final Root actionStatusRoot = queryActionStatus.from(ActionStatus.class); + final CriteriaQuery query = queryActionStatus + .multiselect(actionStatusRoot.get(ActionStatus_.id), actionStatusRoot.get(ActionStatus_.status)) + .where(cb.equal(actionStatusRoot.get(ActionStatus_.action), action)) + .orderBy(cb.desc(actionStatusRoot.get(ActionStatus_.id))); + final List resultList = entityManager.createQuery(query).setFirstResult(0).setMaxResults(1) + .getResultList(); + + // if the latest status is not in retrieve state then we add a retrieved + // state again, we want + // to document a deployment retrieved status and a cancel retrieved + // status, but multiple + // retrieves after the other we don't want to store to protect to + // overflood action status in + // case controller retrieves a action multiple times. + if (resultList.isEmpty() || resultList.get(0)[1] != Status.RETRIEVED) { + // document that the status has been retrieved + actionStatusRepository + .save(new ActionStatus(action, Status.RETRIEVED, System.currentTimeMillis(), message)); + + // don't change the action status itself in case the action is in + // canceling state otherwise + // we modify the action status and the controller won't get the + // cancel job anymore. + if (!action.isCancelingOrCanceled()) { + final Action actionMerge = entityManager.merge(action); + actionMerge.setStatus(Status.RETRIEVED); + return actionRepository.save(actionMerge); + } + } + return action; + } + + /** + * @param statusMessage + */ + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + @Modifying + @Transactional + public void addActionStatusMessage(final ActionStatus statusMessage) { + actionStatusRepository.save(statusMessage); + } + + /** + * An direct access to the security token of an + * {@link Target#getSecurityToken()} without authorization. This is + * necessary to be able to access the security-token without any + * security-context information because the security-token is used for + * authentication. + * + * @param controllerId + * the ID of the controller to retrieve the security token for + * @return the security context of the target, in case no target exists for + * the given controllerId {@code null} is returned + */ + @Transactional + public String getSecurityTokenByControllerId(final String controllerId) { + final Target target = targetRepository.findByControllerId(controllerId); + return target != null ? target.getSecurityToken() : null; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java new file mode 100755 index 000000000..adfb3beb2 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java @@ -0,0 +1,706 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import javax.persistence.EntityManager; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.JoinType; +import javax.persistence.criteria.ListJoin; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.Constants; +import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.eclipse.hawkbit.repository.event.DeploymentManagementEvents; +import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.ActionWithStatusCount; +import org.eclipse.hawkbit.repository.model.Action_; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.DistributionSet_; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetInfo; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.eclipse.hawkbit.repository.specifications.TargetSpecifications; +import org.hibernate.validator.constraints.NotEmpty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import com.google.common.collect.Lists; + +/** + * Business service facade for managing all deployment related data and actions. + * + * + */ +@Transactional(readOnly = true) +@Validated +@Service +public class DeploymentManagement { + private static final Logger LOG = LoggerFactory.getLogger(DeploymentManagement.class); + + @Autowired + private EntityManager entityManager; + + @Autowired + private ActionRepository actionRepository; + + @Autowired + private DistributionSetRepository distributoinSetRepository; + + @Autowired + private SoftwareModuleRepository softwareModuleRepository; + + @Autowired + private TargetRepository targetRepository; + + @Autowired + private ActionStatusRepository actionStatusRepository; + + @Autowired + private TargetManagement targetManagement; + + @Autowired + private TargetInfoRepository targetInfoRepository; + + @Autowired + private AuditorAware auditorProvider; + + @Autowired + private DeploymentManagementEvents deploymentManagementEvents; + + /** + * method assigns the {@link DistributionSet} to all {@link Target}s. + * + * @param pset + * {@link DistributionSet} which is assigned to the + * {@link Target}s + * @param targets + * the {@link Target}s which should obtain the + * {@link DistributionSet} + * + * @return the changed targets + * + * @throw IncompleteDistributionSetException if mandatory + * {@link SoftwareModuleType} are not assigned as define by the + * {@link DistributionSetType}. * + */ + @Transactional + @Modifying + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) + @CacheEvict(value = { "distributionUsageAssigned" }, allEntries = true) + public DistributionSetAssignmentResult assignDistributionSet(@NotNull final DistributionSet pset, + @NotEmpty final List targets) { + + return assignDistributionSetByTargetId(pset, + targets.stream().map(target -> target.getControllerId()).collect(Collectors.toList()), + ActionType.FORCED, Action.NO_FORCE_TIME); + + } + + /** + * method assigns the {@link DistributionSet} to all {@link Target}s by + * their IDs. + * + * @param dsID + * {@link DistributionSet} which is assigned to the + * {@link Target}s + * @param targetIDs + * IDs of the {@link Target}s which should obtain the + * {@link DistributionSet} + * + * @return the changed targets + * + * @throws EntityNotFoundException + * if {@link DistributionSet} does not exist. + * + * @throw IncompleteDistributionSetException if mandatory + * {@link SoftwareModuleType} are not assigned as define by the + * {@link DistributionSetType}. + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) + @CacheEvict(value = { "distributionUsageAssigned" }, allEntries = true) + public DistributionSetAssignmentResult assignDistributionSet(@NotNull final Long dsID, + @NotEmpty final String... targetIDs) { + return assignDistributionSet(dsID, ActionType.FORCED, Action.NO_FORCE_TIME, targetIDs); + } + + /** + * method assigns the {@link DistributionSet} to all {@link Target}s by + * their IDs with a specific {@link ActionType} and {@code forcetime}. + * + * @param dsID + * the ID of the distribution set to assign + * @param actionType + * the type of the action to apply on the assignment + * @param forcedTimestamp + * the time when the action should be forced, only necessary for + * {@link ActionType#TIMEFORCED} + * @param targetIDs + * the IDs of the target to assign the distribution set + * @return the assignment result + * + * @throw IncompleteDistributionSetException if mandatory + * {@link SoftwareModuleType} are not assigned as define by the + * {@link DistributionSetType}. + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) + @CacheEvict(value = { "distributionUsageAssigned" }, allEntries = true) + public DistributionSetAssignmentResult assignDistributionSet(@NotNull final Long dsID, final ActionType actionType, + final long forcedTimestamp, @NotEmpty final String... targetIDs) { + return assignDistributionSet(dsID, Arrays.stream(targetIDs) + .map(t -> new TargetWithActionType(t, actionType, forcedTimestamp)).collect(Collectors.toList())); + } + + /** + * method assigns the {@link DistributionSet} to all {@link Target}s by + * their IDs with a specific {@link ActionType} and {@code forcetime}. + * + * @param dsID + * the ID of the distribution set to assign + * @param targets + * a list of all targets and their action type + * @return the assignment result + * + * @throw IncompleteDistributionSetException if mandatory + * {@link SoftwareModuleType} are not assigned as define by the + * {@link DistributionSetType}. + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) + @CacheEvict(value = { "distributionUsageAssigned" }, allEntries = true) + public DistributionSetAssignmentResult assignDistributionSet(@NotNull final Long dsID, + final List targets) { + final DistributionSet set = distributoinSetRepository.findOne(dsID); + if (set == null) { + throw new EntityNotFoundException( + String.format("no %s with id %d found", DistributionSet.class.getSimpleName(), dsID)); + } + + return assignDistributionSetToTargets(set, targets); + } + + /** + * method assigns the {@link DistributionSet} to all {@link Target}s by + * their IDs with a specific {@link ActionType} and {@code forcetime}. + * + * @param dsID + * the ID of the distribution set to assign + * @param targetsWithActionType + * a list of all targets and their action type + * @return the assignment result + * + * @throw IncompleteDistributionSetException if mandatory + * {@link SoftwareModuleType} are not assigned as define by the + * {@link DistributionSetType}. + */ + private DistributionSetAssignmentResult assignDistributionSetToTargets(@NotNull final DistributionSet set, + final List targetsWithActionType) { + + if (!set.isComplete()) { + throw new IncompleteDistributionSetException( + "Distribution set of type " + set.getType().getKey() + " is incomplete: " + set.getId()); + } + + final List controllerIDs = targetsWithActionType.stream().map(TargetWithActionType::getTargetId) + .collect(Collectors.toList()); + + LOG.debug("assignDistribution({}) to {} targets", set, controllerIDs.size()); + + final Map targetsWithActionMap = targetsWithActionType.stream() + .collect(Collectors.toMap(TargetWithActionType::getTargetId, Function.identity())); + + // split tIDs length into max entries in-statement because many database + // have constraint of + // max entries in in-statements e.g. Oracle with maximum 1000 elements, + // so we need to split + // the entries here and execute multiple statements + // we take the target only into account if the requested operation is no + // duplicate of a + // previous one + final List targets = Lists.partition(controllerIDs, Constants.MAX_ENTRIES_IN_STATEMENT).stream() + .map(ids -> targetRepository + .findAll(TargetSpecifications.hasControllerIdAndAssignedDistributionSetIdNot(ids, set.getId()))) + .flatMap(t -> t.stream()).collect(Collectors.toList()); + + if (targets.isEmpty()) { + // detaching as it is not necessary to persist the set itself + entityManager.detach(set); + // return with nothing as all targets had the DS already assigned + return new DistributionSetAssignmentResult(Collections.emptyList(), 0, targetsWithActionType.size(), + Collections.emptyList(), targetManagement); + } + + final List> targetIds = Lists.partition( + targets.stream().map(Target::getId).collect(Collectors.toList()), Constants.MAX_ENTRIES_IN_STATEMENT); + + // override all active actions and set them into canceling state, we + // need to remember which + // one we have been switched to canceling state because for targets + // which we have changed to + // canceling we don't want to publish the new action update event. + final Set targetIdsCancellList = new HashSet(); + targetIds.forEach(ids -> targetIdsCancellList.addAll(overrideObsoleteUpdateActions(ids))); + + // check for old assigments that needs cancelation + // final List canncelledTargetIds = + + // set assigned distribution set and TargetUpdateStatus + final String currentUser; + if (auditorProvider != null) { + currentUser = auditorProvider.getCurrentAuditor(); + } else { + currentUser = null; + } + + targetIds.forEach(tIds -> targetRepository.setAssignedDistributionSet(set, System.currentTimeMillis(), + currentUser, tIds)); + targetIds.forEach(tIds -> targetInfoRepository.setTargetUpdateStatus(TargetUpdateStatus.PENDING, tIds)); + + final Map targetIdsToActions = actionRepository.save(targets.stream().map(t -> { + final Action tAction = new Action(); + final TargetWithActionType targetWithActionType = targetsWithActionMap.get(t.getControllerId()); + tAction.setActionType(targetWithActionType.getActionType()); + tAction.setForcedTime(targetWithActionType.getForceTime()); + tAction.setActive(true); + tAction.setStatus(Status.RUNNING); + tAction.setTarget(t); + tAction.setDistributionSet(set); + return tAction; + }).collect(Collectors.toList())).stream() + .collect(Collectors.toMap(a -> a.getTarget().getControllerId(), Function.identity())); + + // MECS-720 create initial action status when action is created so we + // remember the initial + // running status because we will change the status of the action itself + // and with this action + // status we have a nicer action history. + targetIdsToActions.values().forEach(action -> { + final ActionStatus actionStatus = new ActionStatus(); + actionStatus.setAction(action); + actionStatus.setOccurredAt(action.getCreatedAt()); + actionStatus.setStatus(Status.RUNNING); + actionStatusRepository.save(actionStatus); + }); + + // select updated targets in order to return them + final DistributionSetAssignmentResult result = new DistributionSetAssignmentResult( + targets.stream().map(target -> target.getControllerId()).collect(Collectors.toList()), targets.size(), + controllerIDs.size() - targets.size(), Lists.newArrayList(targetIdsToActions.values()), + targetManagement); + + LOG.debug("assignDistribution({}) finished {}", set, result); + + final List softwareModules = softwareModuleRepository.findByAssignedTo(set); + + // detaching as it is not necessary to persist the set itself + entityManager.detach(set); + + // send distribution set assignment event + targets.stream().filter(t -> !!!targetIdsCancellList.contains(t.getId())) + .forEach(t -> deploymentManagementEvents.assignDistributionSet(t, + targetIdsToActions.get(t.getControllerId()).getId(), softwareModules)); + + return result; + } + + /** + * Removes {@link UpdateAction}s that are no longer necessary and sends + * cancelations to the controller. + * + * @param myTarget + * to override {@link UpdateAction}s + */ + private Set overrideObsoleteUpdateActions(final List targetsIds) { + + final Set cancelledTargetIds = new HashSet(); + + // Figure out if there are potential target/action combinations that + // need to be considered + // for cancelation + final List activeActions = actionRepository + .findByActiveAndTargetIdInAndActionStatusNotEqualToAndDistributionSetRequiredMigrationStep(targetsIds, + Action.Status.CANCELING); + activeActions.forEach(action -> { + action.setStatus(Status.CANCELING); + // document that the status has been retrieved + actionStatusRepository.save(new ActionStatus(action, Status.CANCELING, System.currentTimeMillis(), + "manual cancelation requested")); + deploymentManagementEvents.cancalAssignDistributionSet(action.getTarget(), action.getId()); + cancelledTargetIds.add(action.getTarget().getId()); + }); + actionRepository.save(activeActions); + + return cancelledTargetIds; + } + + private DistributionSetAssignmentResult assignDistributionSetByTargetId(@NotNull final DistributionSet set, + @NotEmpty final List tIDs, final ActionType actionType, final long forcedTime) { + return assignDistributionSetToTargets(set, tIDs.stream() + .map(t -> new TargetWithActionType(t, actionType, forcedTime)).collect(Collectors.toList())); + } + + /** + * Internal helper method used only inside service level. As a result is no + * additional security necessary. + * + * @param target + * to update + * @param status + * of the target + * @param setInstalledDate + * to set + * + * @return updated target + */ + Target updateTargetInfo(@NotNull final Target target, @NotNull final TargetUpdateStatus status, + final boolean setInstalledDate) { + final TargetInfo ts = target.getTargetInfo(); + ts.setUpdateStatus(status); + + if (setInstalledDate) { + ts.setInstallationDate(System.currentTimeMillis()); + } + targetInfoRepository.save(ts); + return entityManager.merge(target); + } + + /** + * Cancels given {@link Action} for given {@link Target}. The method will + * immediately add a {@link ActionStatus.Status#CANCELED} status to the + * action. However, it might be possible that the controller will continue + * to work on the cancellation. + * + * @param action + * to be canceled + * @param target + * for which the action needs cancellation + * + * @return generated {@link CancelAction} or null if not in + * {@link Target#getActiveActions()}. + * @throws CancelActionNotAllowedException + * in case the given action is not active or is already a cancel + * action + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) + public Action cancelAction(@NotNull final Action action, @NotNull final Target target) { + LOG.debug("cancelAction({}, {})", action, target); + if (action.isCancelingOrCanceled()) { + throw new CancelActionNotAllowedException("Actions in canceling or canceled state cannot be canceled"); + } + final Action myAction = entityManager.merge(action); + + if (myAction.isActive()) { + LOG.debug("action ({}) was still active. Change to {}.", action, Status.CANCELING); + myAction.setStatus(Status.CANCELING); + + // document that the status has been retrieved + actionStatusRepository.save(new ActionStatus(myAction, Status.CANCELING, System.currentTimeMillis(), + "manual cancelation requested")); + + deploymentManagementEvents.cancalAssignDistributionSet(target, myAction.getId()); + + return actionRepository.save(myAction); + } else { + throw new CancelActionNotAllowedException( + "Action [id: " + action.getId() + "] is not active and cannot be canceled"); + } + } + + /** + * Get the {@link Action} entity for given actionId. + * + * @param actionId + * to be id of the action + * @return the corresponding {@link Action} + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Action findAction(@NotNull final Long actionId) { + return actionRepository.findOne(actionId); + } + + /** + * Get the {@link Action} entity for given actionId with all lazy + * attributes. + * + * @param actionId + * to be id of the action + * @return the corresponding {@link Action} + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Action findActionWithDetails(@NotNull final Long actionId) { + return actionRepository.findById(actionId); + } + + /** + * Retrieves all {@link Action}s of a specific target. + * + * @param pageable + * pagination parameter + * @param target + * of which the actions have to be searched + * @return a paged list of actions associated with the given target + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Slice findActionsByTarget(final Pageable pageable, final Target target) { + return actionRepository.findByTarget(pageable, target); + } + + /** + * Retrieves all {@link Action}s of a specific target ordered by action ID. + * + * @param target + * the target associated with the actions + * @return a list of actions associated with the given target ordered by + * action ID + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public List findActionsByTarget(final Target target) { + return actionRepository.findByTarget(target); + } + + /** + * Retrieves all {@link Action}s of a specific target ordered by action ID. + * + * @param target + * the target associated with the actions + * @return a list of actions associated with the given target ordered by + * action ID + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public List findActionsWithStatusCountByTargetOrderByIdDesc(final Target target) { + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + final CriteriaQuery query = cb.createQuery(ActionWithStatusCount.class); + final Root actionRoot = query.from(Action.class); + final ListJoin actionStatusJoin = actionRoot.join(Action_.actionStatus, JoinType.LEFT); + final Join actionDsJoin = actionRoot.join(Action_.distributionSet); + + final CriteriaQuery multiselect = query.distinct(true).multiselect( + actionRoot.get(Action_.id), actionRoot.get(Action_.actionType), actionRoot.get(Action_.active), + actionRoot.get(Action_.forcedTime), actionRoot.get(Action_.status), actionRoot.get(Action_.createdAt), + actionRoot.get(Action_.lastModifiedAt), actionDsJoin.get(DistributionSet_.id), + actionDsJoin.get(DistributionSet_.name), actionDsJoin.get(DistributionSet_.version), + cb.count(actionStatusJoin)); + multiselect.where(cb.equal(actionRoot.get(Action_.target), target)); + multiselect.orderBy(cb.desc(actionRoot.get(Action_.id))); + multiselect.groupBy(actionRoot.get(Action_.id)); + final List resultList = entityManager.createQuery(multiselect).getResultList(); + return resultList; + } + + /** + * Retrieves all {@link Action}s assigned to a specific {@link Target} and a + * given specification. + * + * @param specifiction + * the specification to narrow down the search + * @param target + * the target which must be assigned to the actions + * @param pageable + * the page request + * @return a slice of actions assigned to the specific target and the + * specification + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Slice findActionsByTarget(final Specification specifiction, final Target target, + final Pageable pageable) { + + return actionRepository.findAll(new Specification() { + + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, + final CriteriaBuilder cb) { + return cb.and(specifiction.toPredicate(root, query, cb), cb.equal(root.get(Action_.target), target)); + } + }, pageable); + } + + /** + * @param foundTarget + * @param pageable + * @return + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Slice findActionsByTarget(final Target foundTarget, final Pageable pageable) { + return actionRepository.findByTarget(pageable, foundTarget); + } + + /** + * Retrieves all active {@link Action}s of a specific target ordered by + * action ID. + * + * @param pageable + * the pagination parameter + * @param target + * the target associated with the actions + * @return a paged list of actions associated with the given target + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Page findActiveActionsByTarget(final Pageable pageable, final Target target) { + return actionRepository.findByActiveAndTarget(pageable, target, true); + } + + /** + * Retrieves all active {@link Action}s of a specific target ordered by + * action ID. + * + * @param target + * the target associated with the actions + * @return a list of actions associated with the given target + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public List findActiveActionsByTarget(final Target target) { + return actionRepository.findByActiveAndTarget(target, true); + } + + /** + * Retrieves all inactive {@link Action}s of a specific target ordered by + * action ID. + * + * @param target + * the target associated with the actions + * @return a list of actions associated with the given target + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public List findInActiveActionsByTarget(final Target target) { + return actionRepository.findByActiveAndTarget(target, false); + } + + /** + * Retrieves all inactive {@link Action}s of a specific target ordered by + * action ID. + * + * @param pageable + * the pagination parameter + * @param target + * the target associated with the actions + * @return a paged list of actions associated with the given target + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Page findInActiveActionsByTarget(final Pageable pageable, final Target target) { + return actionRepository.findByActiveAndTarget(pageable, target, false); + } + + /** + * counts all actions associated to a specific target. + * + * @param target + * the target associated to the actions to count + * @return the count value of found actions associated to the target + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Long countActionsByTarget(@NotNull final Target target) { + return actionRepository.countByTarget(target); + } + + /** + * counts all actions associated to a specific target. + * + * @param spec + * the specification to filter the count result + * @param target + * the target associated to the actions to count + * @return the count value of found actions associated to the target + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Long countActionsByTarget(@NotNull final Specification spec, @NotNull final Target target) { + return actionRepository.count(new Specification() { + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, + final CriteriaBuilder cb) { + return cb.and(spec.toPredicate(root, query, cb), cb.equal(root.get(Action_.target), target)); + } + }); + } + + /** + * Updates a {@link TargetAction} and forces the {@link TargetAction} if + * it's not already forced. + * + * @param targetId + * the ID of the target + * @param actionId + * the ID of the action + * @return the updated or the found {@link TargetAction} + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) + public Action forceTargetAction(final Long actionId) { + final Action action = actionRepository.findOne(actionId); + if (action != null && !action.isForced()) { + action.setActionType(ActionType.FORCED); + return actionRepository.save(action); + } + return action; + } + + /** + * retrieves all the {@link ActionStatus} entries of the given + * {@link Action} and {@link Target} in the order latest first. + * + * @param pageReq + * pagination parameter + * @param action + * to be filtered on + * @param withMessages + * to true if {@link ActionStatus#getMessages()} + * need to be fetched. + * @return the corresponding {@link Page} of {@link ActionStatus} + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Page findActionStatusMessagesByActionInDescOrder(final Pageable pageReq, final Action action, + final boolean withMessages) { + if (withMessages) { + return actionStatusRepository.getByActionOrderByIdDesc(pageReq, action); + } else { + return actionStatusRepository.findByActionOrderByIdDesc(pageReq, action); + } + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetAssignmentResult.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetAssignmentResult.java new file mode 100755 index 000000000..d80667024 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetAssignmentResult.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.List; + +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Target; + +/** + * A bean which holds a complex result of an service operation to combine the + * information of an assignment and how much of the assignment has been done and + * how much of the assignments had already been existed. + * + * + * + * + */ +public class DistributionSetAssignmentResult extends AssignmentResult { + + private final List assignedTargets; + private final List actions; + + private final TargetManagement targetManagement; + + /** + * + * Constructor. + * + * @param assignedTargets + * the target objects which have been assigned to the + * distribution set + * @param assigned + * count of the assigned targets + * @param alreadyAssigned + * the count of the already assigned targets + * @param targetManagement + * to retrieve the assigned targets + * @param actions + * of the assignment + * + */ + public DistributionSetAssignmentResult(final List assignedTargets, final int assigned, + final int alreadyAssigned, final List actions, final TargetManagement targetManagement) { + super(assigned, alreadyAssigned); + this.assignedTargets = assignedTargets; + this.actions = actions; + this.targetManagement = targetManagement; + } + + /** + * @return the assignedTargets + */ + public List getAssignedTargets() { + return targetManagement.findTargetsByControllerID(assignedTargets); + } + + /** + * @return the actionId + */ + public List getActions() { + return actions; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFilter.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFilter.java new file mode 100755 index 000000000..a545209e4 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFilter.java @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.Collection; + +import org.eclipse.hawkbit.repository.model.DistributionSetType; + +/** + * Holds distribution set filter parameters. + * + * + * + */ +public final class DistributionSetFilter { + private final Boolean isDeleted; + private final Boolean isComplete; + private final DistributionSetType type; + private final String searchText; + private final Boolean selectDSWithNoTag; + private final Collection tagNames; + private final String assignedTargetId; + private final String installedTargetId; + + /** + * Parametric constructor. + * + * @param builder + * DistributionSetFilterBuilder + */ + public DistributionSetFilter(final DistributionSetFilterBuilder builder) { + this.isDeleted = builder.isDeleted; + this.isComplete = builder.isComplete; + this.type = builder.type; + this.searchText = builder.searchText; + this.selectDSWithNoTag = builder.selectDSWithNoTag; + this.tagNames = builder.tagNames; + this.assignedTargetId = builder.assignedTargetId; + this.installedTargetId = builder.installedTargetId; + } + + /** + * + * Distribution set filter builder. + * + * + * + */ + public static class DistributionSetFilterBuilder { + private Boolean isDeleted; + private Boolean isComplete; + private DistributionSetType type; + private String searchText; + private Boolean selectDSWithNoTag; + private Collection tagNames; + private String assignedTargetId; + private String installedTargetId; + + /** + * Build filter. + * + * @return DistributionSetFilter + */ + public DistributionSetFilter build() { + return new DistributionSetFilter(this); + } + + public DistributionSetFilterBuilder setIsDeleted(final Boolean isDeleted) { + this.isDeleted = isDeleted; + return this; + } + + public DistributionSetFilterBuilder setIsComplete(final Boolean isComplete) { + this.isComplete = isComplete; + return this; + } + + public DistributionSetFilterBuilder setType(final DistributionSetType type) { + this.type = type; + return this; + } + + public DistributionSetFilterBuilder setSearchText(final String searchText) { + this.searchText = searchText; + return this; + } + + public DistributionSetFilterBuilder setSelectDSWithNoTag(final Boolean selectDSWithNoTag) { + this.selectDSWithNoTag = selectDSWithNoTag; + return this; + } + + public DistributionSetFilterBuilder setTagNames(final Collection tagNames) { + this.tagNames = tagNames; + return this; + } + + public DistributionSetFilterBuilder setAssignedTargetId(final String assignedTargetId) { + this.assignedTargetId = assignedTargetId; + return this; + } + + public DistributionSetFilterBuilder setInstalledTargetId(final String installedTargetId) { + this.installedTargetId = installedTargetId; + return this; + } + + } + + public Boolean getIsDeleted() { + return isDeleted; + } + + public Boolean getIsComplete() { + return isComplete; + } + + public DistributionSetType getType() { + return type; + } + + public String getSearchText() { + return searchText; + } + + public Boolean getSelectDSWithNoTag() { + return selectDSWithNoTag; + } + + public Collection getTagNames() { + return tagNames; + } + + public String getAssignedTargetId() { + return assignedTargetId; + } + + public String getInstalledTargetId() { + return installedTargetId; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetManagement.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetManagement.java new file mode 100755 index 000000000..e962978a4 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetManagement.java @@ -0,0 +1,1069 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.persistence.Entity; +import javax.persistence.EntityManager; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.eclipse.hawkbit.repository.DistributionSetFilter.DistributionSetFilterBuilder; +import org.eclipse.hawkbit.repository.exception.DistributionSetCreationFailedMissingMandatoryModuleException; +import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.exception.EntityLockedException; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; +import org.eclipse.hawkbit.repository.model.DistributionSetMetadata_; +import org.eclipse.hawkbit.repository.model.DistributionSetTag; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.DistributionSetTypeElement; +import org.eclipse.hawkbit.repository.model.DistributionSet_; +import org.eclipse.hawkbit.repository.model.DsMetadataCompositeKey; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.repository.model.Tag; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.specifications.DistributionSetSpecification; +import org.eclipse.hawkbit.repository.specifications.DistributionSetTypeSpecification; +import org.hibernate.validator.constraints.NotEmpty; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.domain.Specifications; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import com.google.common.base.Strings; + +/** + * Business facade for managing the {@link DistributionSet}s. + * + * + * + * + */ +@Transactional(readOnly = true) +@Validated +@Service +public class DistributionSetManagement { + + @Autowired + private EntityManager entityManager; + + @Autowired + private DistributionSetRepository distributionSetRepository; + + @Autowired + private TagManagement tagManagement; + + @Autowired + private SystemManagement systemManagement; + + @Autowired + private DistributionSetTypeRepository distributionSetTypeRepository; + + @Autowired + private DistributionSetMetadataRepository distributionSetMetadataRepository; + + @Autowired + private ActionRepository actionRepository; + + /** + * Find {@link DistributionSet} based on given ID including (lazy loaded) + * details, e.g. {@link DistributionSet#getAgentHub()}. + * + * Note: for performance reasons it is recommended to use + * {@link #findDistributionSetById(Long)} if details are not necessary. + * + * @param distid + * to look for. + * @return {@link DistributionSet} or null if it does not exist + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public DistributionSet findDistributionSetByIdWithDetails(@NotNull final Long distid) { + return distributionSetRepository.findOne(DistributionSetSpecification.byId(distid)); + } + + /** + * Find {@link DistributionSet} based on given ID without details, e.g. + * {@link DistributionSet#getAgentHub()}. + * + * @param distid + * to look for. + * @return {@link DistributionSet} or null if it does not exist + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public DistributionSet findDistributionSetById(@NotNull final Long distid) { + return distributionSetRepository.findOne(distid); + } + + /** + * {@link Entity} based method call for + * {@link #toggleTagAssignment(Collection, String)}. + * + * @param sets + * to toggle for + * @param tag + * to toogle + * @return {@link DistributionSetTagAssigmentResult} with all metadata of + * the assignment outcome. + */ + @Modifying + @Transactional + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public DistributionSetTagAssigmentResult toggleTagAssignment(@NotEmpty final List sets, + @NotNull final DistributionSetTag tag) { + return toggleTagAssignment(sets.stream().map(ds -> ds.getId()).collect(Collectors.toList()), tag.getName()); + } + + /** + * Toggles {@link DistributionSetTag} assignment to given + * {@link DistributionSet}s by means that if some (or all) of the targets in + * the list have the {@link Tag} not yet assigned, they will be. If all of + * theme have the tag already assigned they will be removed instead. + * + * @param dsIds + * to toggle for + * @param tagName + * to toggle + * @return {@link DistributionSetTagAssigmentResult} with all metadata of + * the assignment outcome. + */ + @Modifying + @Transactional + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public DistributionSetTagAssigmentResult toggleTagAssignment(@NotEmpty final Collection dsIds, + @NotNull final String tagName) { + + final Iterable sets = findDistributionSetListWithDetails(dsIds); + final DistributionSetTag myTag = tagManagement.findDistributionSetTag(tagName); + + DistributionSetTagAssigmentResult result = null; + final List allDSs = new ArrayList<>(); + for (final DistributionSet set : sets) { + if (set.getTags().add(myTag)) { + allDSs.add(set); + } + } + + // unassigment case + if (allDSs.isEmpty()) { + for (final DistributionSet set : sets) { + if (set.getTags().remove(myTag)) { + allDSs.add(set); + } + } + result = new DistributionSetTagAssigmentResult(dsIds.size() - allDSs.size(), 0, allDSs.size(), null, + distributionSetRepository.save(allDSs)); + } else { + result = new DistributionSetTagAssigmentResult(dsIds.size() - allDSs.size(), allDSs.size(), 0, + distributionSetRepository.save(allDSs), null); + } + + // no reason to persist the tag + entityManager.detach(myTag); + return result; + } + + /** + * Retrieves {@link DistributionSet} List including details information, + * i.e. @link BaseSoftwareModule}s and {@link DistributionSetTag}s. + * + * @param distributionIdSet + * List of {@link DistributionSet} IDs to be found + * @return the found {@link DistributionSet}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public List findDistributionSetListWithDetails( + @NotEmpty final Collection distributionIdSet) { + return distributionSetRepository.findAll(DistributionSetSpecification.byIds(distributionIdSet)); + } + + /** + * Updates existing {@link DistributionSet}. + * + * @param ds + * to update + * @return the saved {@link Entity}. + * @throws NullPointerException + * of {@link DistributionSet#getId()} is null + * @throw DataDependencyViolationException in case of illegal update + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public DistributionSet updateDistributionSet(@NotNull final DistributionSet ds) { + checkNotNull(ds.getId()); + final DistributionSet persisted = findDistributionSetByIdWithDetails(ds.getId()); + checkDistributionSetSoftwareModulesIsAllowedToModify(ds, persisted.getModules()); + return distributionSetRepository.save(ds); + } + + /** + *

+ * {@link DistributionSet} can be deleted/erased from the repository if they + * have never been assigned to any {@link UpdateAction} or {@link Target}. + *

+ * + *

+ * If they have been assigned that need to be marked as deleted which as a + * result means that they cannot be assigned anymore to any targets. (define + * e.g. findByDeletedFalse()) + *

+ * + * @param set + * to delete + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_DELETE_REPOSITORY) + public void deleteDistributionSet(@NotNull final DistributionSet set) { + this.deleteDistributionSet(set.getId()); + } + + /** + * Deleted {@link DistributionSet}s by their IDs. That is either a soft + * delete of the entities have been linked to an {@link UpdateAction} before + * or a hard delete if not. + * + * @param distributionSetIDs + * to be deleted + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_DELETE_REPOSITORY) + public void deleteDistributionSet(@NotEmpty final Long... distributionSetIDs) { + final List toHardDelete = new ArrayList<>(); + + final List assigned = distributionSetRepository.findAssignedDistributionSetsById(distributionSetIDs); + + // soft delete assigned + if (!assigned.isEmpty()) { + distributionSetRepository.deleteDistributionSet(assigned.toArray(new Long[assigned.size()])); + } + + // mark the rest as hard delete + for (final Long setId : distributionSetIDs) { + if (!assigned.contains(setId)) { + toHardDelete.add(setId); + } + } + + // hard delete the rest if exixts + if (!toHardDelete.isEmpty()) { + // don't give the delete statement an empty list, JPA/Oracle cannot + // handle the empty list, + // see MECS-403 + distributionSetRepository.deleteByIdIn(toHardDelete); + } + } + + /** + * Creates a new {@link DistributionSet}. + * + * @param dSet + * {@link DistributionSet} to be created + * @return the new persisted {@link DistributionSet} + * + * @throws EntityAlreadyExistsException + * if a given entity already exists + * @throws DistributionSetCreationFailedMissingMandatoryModuleException + * is {@link DistributionSet} does not contain mandatory + * {@link SoftwareModule}s. + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_REPOSITORY) + public DistributionSet createDistributionSet(@NotNull final DistributionSet dSet) { + prepareDsSave(dSet); + + if (dSet.getType() == null) { + dSet.setType(systemManagement.getTenantMetadata().getDefaultDsType()); + } + + final DistributionSet result = distributionSetRepository.save(dSet); + + return result; + } + + private void prepareDsSave(final DistributionSet dSet) { + if (dSet.getId() != null) { + throw new EntityAlreadyExistsException("Parameter seems to be an existing, already persisted entity"); + } + + if (distributionSetRepository.countByNameAndVersion(dSet.getName(), dSet.getVersion()) > 0) { + throw new EntityAlreadyExistsException("DistributionSet with that name and version already exists."); + } + + } + + /** + * Creates multiple {@link DistributionSet}s. + * + * @param distributionSets + * to be created + * @return the new {@link DistributionSet}s + * @throws EntityAlreadyExistsException + * if a given entity already exists + * @throws DistributionSetCreationFailedMissingMandatoryModuleException + * is {@link DistributionSet} does not contain mandatory + * {@link SoftwareModule}s. + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_REPOSITORY) + public List createDistributionSets(@NotNull final Iterable distributionSets) { + for (final DistributionSet ds : distributionSets) { + prepareDsSave(ds); + } + return distributionSetRepository.save(distributionSets); + } + + /** + * Assigns {@link SoftwareModule} to existing {@link DistributionSet}. + * + * @param ds + * to assign and update + * @param softwareModules + * to get assigned + * @return the updated {@link DistributionSet}. + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public DistributionSet assignSoftwareModules(@NotNull final DistributionSet ds, + final Set softwareModules) { + checkDistributionSetSoftwareModulesIsAllowedToModify(ds, softwareModules); + for (final SoftwareModule softwareModule : softwareModules) { + ds.addModule(softwareModule); + } + return distributionSetRepository.save(ds); + } + + /** + * Unassigns a {@link SoftwareModule} form an existing + * {@link DistributionSet}. + * + * @param ds + * to get unassigned form + * @param softwareModule + * to get unassigned + * @return the updated {@link DistributionSet}. + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public DistributionSet unassignSoftwareModule(@NotNull final DistributionSet ds, + final SoftwareModule softwareModule) { + final Set softwareModules = new HashSet(); + softwareModules.add(softwareModule); + ds.removeModule(softwareModule); + checkDistributionSetSoftwareModulesIsAllowedToModify(ds, softwareModules); + return distributionSetRepository.save(ds); + } + + /** + * Updates existing {@link DistributionSetType}. However, keep in mind that + * is not possible to change the {@link DistributionSetTypeElement}s while + * the DS type is already in use. + * + * @param dsType + * to update + * @return updated {@link Entity} + * + * @throws EntityReadOnlyException + * if use tries to change the {@link DistributionSetTypeElement} + * s while the DS type is already in use. + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public DistributionSetType updateDistributionSetType(@NotNull final DistributionSetType dsType) { + checkNotNull(dsType.getId()); + + final DistributionSetType persisted = distributionSetTypeRepository.findOne(dsType.getId()); + + // throw exception if user tries to update a DS type that is already in + // use + if (!persisted.areModuleEntriesIdentical(dsType) && distributionSetRepository.countByType(persisted) > 0) { + throw new EntityReadOnlyException( + String.format("distribution set type %s set is already assigned to targets and cannot be changed", + dsType.getName())); + } + + return distributionSetTypeRepository.save(dsType); + } + + /** + * Generic predicate based query for {@link DistributionSetType}. + * + * @param spec + * of the search + * @param pageable + * parametsr for paging + * + * @return the found {@link SoftwareModuleType}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Page findDistributionSetTypesByPredicate( + @NotNull final Specification spec, @NotNull final Pageable pageable) { + return distributionSetTypeRepository.findAll(spec, pageable); + } + + /** + * @param pageable + * parameter + * @return all {@link DistributionSetType}s in the repository. + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Page findDistributionSetTypesAll(@NotNull final Pageable pageable) { + return distributionSetTypeRepository.findByDeleted(pageable, false); + } + + /** + * retrieves {@link DistributionSet}s by filtering on the given parameters. + * + * @param pageable + * page parameter + * @param distributionSetFilter + * has details of filters to be applied. + * @return the page of found {@link DistributionSet} + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Page findDistributionSetsByFilters(@NotNull final Pageable pageable, + final DistributionSetFilter distributionSetFilter) { + final List> specList = buildDistributionSetSpecifications(distributionSetFilter); + return findByCriteriaAPI(pageable, specList); + } + + /** + * + * @param distributionSetFilter + * had details of filters to be applied + * @return a single DistributionSet which is either installed or assigned to + * a specific target or {@code null}. + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + private DistributionSet findDistributionSetsByFiltersAndInstalledOrAssignedTarget( + final DistributionSetFilter distributionSetFilter) { + + final List> specList = buildDistributionSetSpecifications(distributionSetFilter); + + Specifications specs = null; + if (!specList.isEmpty()) { + specs = Specifications.where(specList.get(0)); + } + if (specList.size() > 1) { + specList.remove(0); + for (final Specification s : specList) { + specs = specs.and(s); + } + } + return distributionSetRepository.findOne(specs); + } + + /** + * finds all {@link DistributionSet}s. + * + * @param pageReq + * the pagination parameter + * @param deleted + * if TRUE, {@link DistributionSet}s marked as deleted are + * returned. If FALSE, on {@link DistributionSet}s with + * {@link DistributionSet#isDeleted()} == FALSE are returned. + * null if both are to be returned + * @param complete + * to true for returning only completed distribution + * sets or false for only incomplete ones nor + * null to return both.. + * @param complete + * set to if false uncomplete DS should also be + * shown. + * + * + * @return all found {@link DistributionSet}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Page findDistributionSetsAll(@NotNull final Pageable pageReq, final Boolean deleted, + final Boolean complete) { + final List> specList = new ArrayList>(); + + if (deleted != null) { + final Specification spec = DistributionSetSpecification.isDeleted(deleted); + specList.add(spec); + } + + if (complete != null) { + final Specification spec = DistributionSetSpecification.isCompleted(complete); + specList.add(spec); + } + + return findByCriteriaAPI(pageReq, specList); + } + + /** + * finds all {@link DistributionSet}s. + * + * @param spec + * the specification to add for the search query. + * @param pageReq + * the pagination parameter + * @param deleted + * if TRUE, {@link DistributionSet}s marked as deleted are + * returned. If FALSE, on {@link DistributionSet}s with + * {@link DistributionSet#isDeleted()} == FALSE are returned. + * null if both are to be returned + * @return all found {@link DistributionSet}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Page findDistributionSetsAll(@NotNull final Specification spec, + @NotNull final Pageable pageReq, final Boolean deleted) { + final List> specList = new ArrayList>(); + if (deleted != null) { + specList.add(DistributionSetSpecification.isDeleted(deleted)); + } + specList.add(spec); + return findByCriteriaAPI(pageReq, specList); + } + + /** + * method retrieves all {@link DistributionSet}s from the repo in the + * following order: + *

+ * 1) {@link DistributionSet}s which have the given {@link Target} as + * {@link TargetStatus#getInstalledDistributionSet()} + *

+ * 2) {@link DistributionSet}s which have the given {@link Target} as + * {@link Target#getAssignedDistributionSet()} + *

+ * 3) {@link DistributionSet}s which have no connection to the given + * {@link Target} ordered by ID of the DistributionSet. + * + * @param pageable + * the page request to page the result set * + * @param distributionSetFilterBuilder + * has details of filters to be applied + * @param assignedOrInstalled + * the ID of the Target to be ordered by + * @return + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Page findDistributionSetsAllOrderedByLinkTarget(@NotNull final Pageable pageable, + @NotNull final DistributionSetFilterBuilder distributionSetFilterBuilder, + @NotNull final String assignedOrInstalled) { + + final DistributionSetFilter filterWithInstalledTargets = distributionSetFilterBuilder + .setInstalledTargetId(assignedOrInstalled).setAssignedTargetId(null).build(); + final DistributionSet installedDS = findDistributionSetsByFiltersAndInstalledOrAssignedTarget( + filterWithInstalledTargets); + + final DistributionSetFilter filterWithAssignedTargets = distributionSetFilterBuilder.setInstalledTargetId(null) + .setAssignedTargetId(assignedOrInstalled).build(); + final DistributionSet assignedDS = findDistributionSetsByFiltersAndInstalledOrAssignedTarget( + filterWithAssignedTargets); + + final DistributionSetFilter dsFilterWithNoTargetLinked = distributionSetFilterBuilder.setInstalledTargetId(null) + .setAssignedTargetId(null).build(); + // first fine the distribution sets filtered by the given filter + // parameters + final Page findDistributionSetsByFilters = findDistributionSetsByFilters(pageable, + dsFilterWithNoTargetLinked); + + final List resultSet = new LinkedList<>(findDistributionSetsByFilters.getContent()); + int orderIndex = 0; + if (installedDS != null) { + final boolean remove = resultSet.remove(installedDS); + if (!remove) { + resultSet.remove(resultSet.size() - 1); + } + resultSet.add(orderIndex, installedDS); + orderIndex++; + } + if (assignedDS != null && !assignedDS.equals(installedDS)) { + final boolean remove = resultSet.remove(assignedDS); + if (!remove) { + resultSet.remove(resultSet.size() - 1); + } + resultSet.add(orderIndex, assignedDS); + } + + return new PageImpl<>(resultSet, pageable, findDistributionSetsByFilters.getTotalElements()); + } + + private Long countDistributionSetByCriteriaAPI(@NotEmpty final List> specList) { + Specifications specs = Specifications.where(specList.get(0)); + if (specList.size() > 1) { + for (final Specification s : specList.subList(1, specList.size())) { + specs = specs.and(s); + } + } + + return distributionSetRepository.count(specs); + } + + /** + * Find distribution set by name and version. + * + * @param distributionName + * name of {@link DistributionSet}; case insensitive + * @param version + * version of {@link DistributionSet} + * @return the page with the found {@link DistributionSet} + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public DistributionSet findDistributionSetByNameAndVersion(@NotEmpty final String distributionName, + @NotEmpty final String version) { + final Specification spec = DistributionSetSpecification + .equalsNameAndVersionIgnoreCase(distributionName, version); + return distributionSetRepository.findOne(spec); + + } + + /** + * Retrieves {@link DistributionSet} List for overview purposes (no + * {@link SoftwareModule}s and {@link DistributionSetTag}s). + * + * Please use {@link #findDistributionSetListWithDetails(Iterable)} if + * details are required. + * + * @param dist + * List of {@link DistributionSet} IDs to be found + * @return the found {@link DistributionSet}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Iterable findDistributionSetList(@NotEmpty final Collection dist) { + return distributionSetRepository.findAll(dist); + } + + /** + * Count all {@link DistributionSet}s in the repository that are not marked + * as deleted. + * + * @return number of {@link DistributionSet}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Long countDistributionSetsAll() { + + final List> specList = new ArrayList>(); + + final Specification spec = DistributionSetSpecification.isDeleted(Boolean.FALSE); + specList.add(spec); + + return countDistributionSetByCriteriaAPI(specList); + } + + /** + * @return number of {@link DistributionSetType}s in the repository. + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Long countDistributionSetTypesAll() { + return distributionSetTypeRepository.countByDeleted(false); + } + + /** + * @param name + * as {@link DistributionSetType#getName()} + * @return {@link DistributionSetType} if found or null if not + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public DistributionSetType findDistributionSetTypeByName(@NotNull final String name) { + return distributionSetTypeRepository.findOne(DistributionSetTypeSpecification.byName(name)); + } + + /** + * @param id + * as {@link DistributionSetType#getId()} + * @return {@link DistributionSetType} if found or null if not + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public DistributionSetType findDistributionSetTypeById(@NotNull final Long id) { + return distributionSetTypeRepository.findOne(DistributionSetTypeSpecification.byId(id)); + } + + /** + * @param key + * as {@link DistributionSetType#getKey()} + * @return {@link DistributionSetType} if found or null if not + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public DistributionSetType findDistributionSetTypeByKey(@NotNull final String key) { + return distributionSetTypeRepository.findOne(DistributionSetTypeSpecification.byKey(key)); + } + + /** + * Creates new {@link DistributionSetType}. + * + * @param type + * to create + * @return created {@link Entity} + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_REPOSITORY) + public DistributionSetType createDistributionSetType(@NotNull final DistributionSetType type) { + if (type.getId() != null) { + throw new EntityAlreadyExistsException("Given type contains an Id!"); + } + + return distributionSetTypeRepository.save(type); + } + + /** + * Deletes or markes as delete in case the type is in use. + * + * @param type + * to delete + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_DELETE_REPOSITORY) + public void deleteDistributionSetType(@NotNull final DistributionSetType type) { + + if (distributionSetRepository.countByType(type) > 0) { + final DistributionSetType toDelete = entityManager.merge(type); + toDelete.setDeleted(true); + distributionSetTypeRepository.save(toDelete); + } else { + distributionSetTypeRepository.delete(type.getId()); + } + } + + /** + * creates or updates a single distribution set meta data entry. + * + * @param metadata + * the meta data entry to create or update + * @return the updated or created distribution set meta data entry + * @throws EntityAlreadyExistsException + * in case the meta data entry already exists for the specific + * key + */ + @Transactional + @Modifying + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public DistributionSetMetadata createDistributionSetMetadata(@NotNull final DistributionSetMetadata metadata) { + if (distributionSetMetadataRepository.exists(metadata.getId())) { + throwMetadataKeyAlreadyExists(metadata.getId().getKey()); + } + // merge base software module so optLockRevision gets updated and audit + // log written because + // modifying metadata is modifying the base distribution set itself for + // auditing purposes. + entityManager.merge(metadata.getDistributionSet()).setLastModifiedAt(0L); + return distributionSetMetadataRepository.save(metadata); + } + + /** + * creates a list of distribution set meta data entries. + * + * @param metadata + * the meta data entries to create or update + * @return the updated or created distribution set meta data entries + * @throws EntityAlreadyExistsException + * in case one of the meta data entry already exists for the + * specific key + */ + @Transactional + @Modifying + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public List createDistributionSetMetadata( + @NotEmpty final Collection metadata) { + for (final DistributionSetMetadata distributionSetMetadata : metadata) { + checkAndThrowAlreadyIfDistributionSetMetadataExists(distributionSetMetadata.getId()); + } + metadata.forEach(m -> entityManager.merge(m.getDistributionSet()).setLastModifiedAt(-1L)); + return (List) distributionSetMetadataRepository.save(metadata); + } + + /** + * updates a distribution set meta data value if corresponding entry exists. + * + * @param metadata + * the meta data entry to be updated + * @return the updated meta data entry + * @throws EntityNotFoundException + * in case the meta data entry does not exists and cannot be + * updated + */ + @Transactional + @Modifying + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public DistributionSetMetadata updateDistributionSetMetadata(@NotNull final DistributionSetMetadata metadata) { + // check if exists otherwise throw entity not found exception + findOne(metadata.getId()); + // touch it to update the lock revision because we are modifying the + // DS indirectly + entityManager.merge(metadata.getDistributionSet()).setLastModifiedAt(0L); + return distributionSetMetadataRepository.save(metadata); + } + + /** + * deletes a distribution set meta data entry. + * + * @param id + * the ID of the distribution set meta data to delete + */ + @Transactional + @Modifying + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public void deleteDistributionSetMetadata(@NotNull final DsMetadataCompositeKey id) { + distributionSetMetadataRepository.delete(id); + } + + /** + * finds all meta data by the given distribution set id. + * + * @param distributionSetId + * the distribution set id to retrieve the meta data from + * @param pageable + * the page request to page the result + * @return a paged result of all meta data entries for a given distribution + * set id + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Page findDistributionSetMetadataByDistributionSetId( + @NotNull final Long distributionSetId, @NotNull final Pageable pageable) { + + return distributionSetMetadataRepository.findAll(new Specification() { + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, + final CriteriaBuilder cb) { + + final Predicate predicate = cb.equal( + root.get(DistributionSetMetadata_.distributionSet).get(DistributionSet_.id), distributionSetId); + + return predicate; + } + }, pageable); + + } + + /** + * finds all meta data by the given distribution set id. + * + * @param distributionSetId + * the distribution set id to retrieve the meta data from + * @param spec + * the specification to filter the result + * @param pageable + * the page request to page the result + * @return a paged result of all meta data entries for a given distribution + * set id + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Page findDistributionSetMetadataByDistributionSetId( + @NotNull final Long distributionSetId, @NotNull final Specification spec, + @NotNull final Pageable pageable) { + return distributionSetMetadataRepository.findAll(new Specification() { + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, + final CriteriaBuilder cb) { + return cb.and(cb.equal(root.get(DistributionSetMetadata_.distributionSet).get(DistributionSet_.id), + distributionSetId), spec.toPredicate(root, query, cb)); + } + }, pageable); + } + + /** + * finds a single distribution set meta data by its id. + * + * @param id + * the id of the distribution set meta data containing the meta + * data key and the ID of the distribution set + * @return the found DistributionSetMetadata or {@code null} if not exits + * @throws EntityNotFoundException + * in case the meta data does not exists for the given key + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public DistributionSetMetadata findOne(@NotNull final DsMetadataCompositeKey id) { + final DistributionSetMetadata findOne = distributionSetMetadataRepository.findOne(id); + if (findOne == null) { + throw new EntityNotFoundException("Metadata with key '" + id.getKey() + "' does not exist"); + } + return findOne; + } + + /** + * retrieves the distribution set for a given action. + * + * @param action + * the action associated with the distribution set + * @return the distribution set which is associated with the action + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public DistributionSet findDistributionSetByAction(@NotNull final Action action) { + return distributionSetRepository.findByAction(action); + } + + /** + * Creates multiple {@link DistributionSetType}s. + * + * @param types + * to create + * @return created {@link Entity} + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_REPOSITORY) + public List createDistributionSetTypes(@NotNull final Collection types) { + return types.stream().map(type -> createDistributionSetType(type)).collect(Collectors.toList()); + } + + /** + * Checking Distribution Set is already using while assign Software module. + * + * @param distributionSet + * @param softwareModules + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public void checkDistributionSetAlreadyUse(final DistributionSet distributionSet) { + checkDistributionSetSoftwareModulesIsAllowedToModify(distributionSet); + } + + private List> buildDistributionSetSpecifications( + final DistributionSetFilter distributionSetFilter) { + final List> specList = new ArrayList>(); + + Specification spec = null; + + if (null != distributionSetFilter.getIsComplete()) { + spec = DistributionSetSpecification.isCompleted(distributionSetFilter.getIsComplete()); + specList.add(spec); + } + + if (null != distributionSetFilter.getIsDeleted()) { + spec = DistributionSetSpecification.isDeleted(distributionSetFilter.getIsDeleted()); + specList.add(spec); + } + + if (distributionSetFilter.getType() != null) { + spec = DistributionSetSpecification.byType(distributionSetFilter.getType()); + specList.add(spec); + } + + if (!Strings.isNullOrEmpty(distributionSetFilter.getSearchText())) { + spec = DistributionSetSpecification.likeNameOrDescriptionOrVersion(distributionSetFilter.getSearchText()); + specList.add(spec); + } + + if (isDSWithNoTagSelected(distributionSetFilter) || isTagsSelected(distributionSetFilter)) { + spec = DistributionSetSpecification.hasTags(distributionSetFilter.getTagNames(), + distributionSetFilter.getSelectDSWithNoTag()); + specList.add(spec); + } + if (distributionSetFilter.getInstalledTargetId() != null) { + spec = DistributionSetSpecification.installedTarget(distributionSetFilter.getInstalledTargetId()); + specList.add(spec); + } + if (distributionSetFilter.getAssignedTargetId() != null) { + spec = DistributionSetSpecification.assignedTarget(distributionSetFilter.getAssignedTargetId()); + specList.add(spec); + } + return specList; + } + + private void checkDistributionSetSoftwareModulesIsAllowedToModify(final DistributionSet distributionSet, + final Set softwareModules) { + if (!new HashSet(distributionSet.getModules()).equals(softwareModules) + && actionRepository.countByDistributionSet(distributionSet) > 0) { + throw new EntityLockedException( + String.format("distribution set %s:%s is already assigned to targets and cannot be changed", + distributionSet.getName(), distributionSet.getVersion())); + } + } + + private void checkDistributionSetSoftwareModulesIsAllowedToModify(final DistributionSet distributionSet) { + if (actionRepository.countByDistributionSet(distributionSet) > 0) { + throw new EntityLockedException( + String.format("distribution set %s:%s is already assigned to targets and cannot be changed", + distributionSet.getName(), distributionSet.getVersion())); + } + } + + private Boolean isDSWithNoTagSelected(final DistributionSetFilter distributionSetFilter) { + if (distributionSetFilter.getSelectDSWithNoTag() != null && distributionSetFilter.getSelectDSWithNoTag()) { + return true; + } + return false; + } + + private Boolean isTagsSelected(final DistributionSetFilter distributionSetFilter) { + if (distributionSetFilter.getTagNames() != null && !distributionSetFilter.getTagNames().isEmpty()) { + return true; + } + return false; + } + + /** + * executes findAll with the given {@link DistributionSet} + * {@link Specification}s. + * + * @param pageable + * paging parameter + * @param specList + * list of @link {@link Specification} + * @return the page with the found {@link DistributionSet} + */ + private Page findByCriteriaAPI(@NotNull final Pageable pageable, + final List> specList) { + Specifications specs = null; + if (!specList.isEmpty()) { + specs = Specifications.where(specList.get(0)); + } + if (specList.size() > 1) { + for (final Specification s : specList.subList(1, specList.size())) { + specs = specs.and(s); + } + } + + if (specs == null) { + return distributionSetRepository.findAll(pageable); + } else { + return distributionSetRepository.findAll(specs, pageable); + } + } + + private void checkAndThrowAlreadyIfDistributionSetMetadataExists(final DsMetadataCompositeKey metadataId) { + if (distributionSetMetadataRepository.exists(metadataId)) { + throw new EntityAlreadyExistsException( + "Metadata entry with key '" + metadataId.getKey() + "' already exists"); + } + } + + private void throwMetadataKeyAlreadyExists(final String metadataKey) { + throw new EntityAlreadyExistsException("Metadata entry with key '" + metadataKey + "' already exists"); + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetMetadataRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetMetadataRepository.java new file mode 100755 index 000000000..1ff6c1ca5 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetMetadataRepository.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; +import org.eclipse.hawkbit.repository.model.DsMetadataCompositeKey; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.transaction.annotation.Transactional; + +/** + * {@link DistributionSetMetadata} repository. + * + * + * + */ +@Transactional(readOnly = true) +public interface DistributionSetMetadataRepository + extends PagingAndSortingRepository, + JpaSpecificationExecutor { + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetRepository.java new file mode 100755 index 000000000..1a59fdca7 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetRepository.java @@ -0,0 +1,135 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.Collection; +import java.util.List; + +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetTag; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.Tag; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; + +/** + * {@link DistributionSet} repository. + * + * + * + */ +@Transactional(readOnly = true) +public interface DistributionSetRepository + extends BaseEntityRepository, JpaSpecificationExecutor { + + /** + * Finds {@link DistributionSet}s by assigned {@link Tag}. + * + * @param tag + * to be found + * @return list of found {@link DistributionSet}s + */ + @Query(value = "Select Distinct ds from DistributionSet ds join ds.tags dst where dst = :tag") + List findByTag(@Param("tag") final DistributionSetTag tag); + + /** + * deletes the {@link DistributionSet}s with the given IDs. + * + * @param ids + * to be deleted + */ + @Modifying + @Transactional + @Query("update DistributionSet d set d.deleted = 1 where d.id in :ids") + void deleteDistributionSet(@Param("ids") Long... ids); + + /** + * deletes {@link DistributionSet}s by the given IDs. + * + * @param ids + * List of IDs of {@link DistributionSet}s to be deleted + * @return number of affected/deleted records + */ + @Modifying + @Transactional + // Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=349477 + @Query("DELETE FROM DistributionSet d WHERE d.id IN ?1") + int deleteByIdIn(Collection ids); + + /** + * Finds {@link DistributionSet}s where given {@link SoftwareModule} is + * assigned. + * + * @param module + * to search for + * @return {@link List} of found {@link DistributionSet}s + */ + List findByModules(SoftwareModule module); + + /** + * Finds {@link DistributionSet}s based on given ID if they are not assigned + * yet to an {@link UpdateAction}, i.e. unused. + * + * @param ids + * to searcgh for + * @return + */ + @Query("select ac.distributionSet.id from Action ac where ac.distributionSet.id in :ids") + List findAssignedDistributionSetsById(@Param("ids") Long... ids); + + /** + * Saves all given {@link DistributionSet}s. + * + * @param entities + * @return the saved entities + * @throws IllegalArgumentException + * in case the given entity is (@literal null}. + * + * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable) + */ + @Override + List save(Iterable entities); + + /** + * Finds the distribution set for a specific action. + * + * @param action + * the action associated with the distribution set to find + * @return the distribution set associated with the given action + */ + @Query("select DISTINCT d from DistributionSet d join fetch d.modules m join d.actions a where a = :action") + DistributionSet findByAction(@Param("action") Action action); + + /** + * Counts {@link DistributionSet} instances of given type in the repository. + * + * @param type + * to search for + * @return number of found {@link DistributionSet}s + */ + long countByType(DistributionSetType type); + + /** + * Counts {@link DistributionSet} with given + * {@link DistributionSet#getName()} and + * {@link DistributionSet#getVersion()}. + * + * @param name + * to search for + * @param version + * to search for + * @return number of found {@link DistributionSet}s + */ + long countByNameAndVersion(String name, String version); +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTagAssigmentResult.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTagAssigmentResult.java new file mode 100755 index 000000000..4ece58344 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTagAssigmentResult.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.List; + +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetTag; + +/** + * Result object for {@link DistributionSetTag} assigments. + * + * + * + * + */ +public class DistributionSetTagAssigmentResult extends AssignmentResult { + + private final int unassigned; + private final List assignedDs; + private final List unassignedDs; + + /** + * Constructor. + * + * @param alreadyAssigned + * number of already assigned/ignored elements + * @param assigned + * number of newly assigned elements + * @param unassigned + * number of newly assigned elements + * @param assignedDs + * {@link List} of assigned {@link DistributionSet}s. + * @param unassignedDs + * {@link List} of unassigned {@link DistributionSet}s. + */ + public DistributionSetTagAssigmentResult(final int alreadyAssigned, final int assigned, final int unassigned, + final List assignedDs, final List unassignedDs) { + super(assigned, alreadyAssigned); + this.unassigned = unassigned; + this.assignedDs = assignedDs; + this.unassignedDs = unassignedDs; + } + + /** + * @return the unassigned + */ + public int getUnassigned() { + return unassigned; + } + + /** + * @return the assignedDs + */ + public List getAssignedDs() { + return assignedDs; + } + + /** + * @return the unassignedDs + */ + public List getUnassignedDs() { + return unassignedDs; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTagRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTagRepository.java new file mode 100755 index 000000000..39770dee0 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTagRepository.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.List; + +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetTag; +import org.eclipse.hawkbit.repository.model.TargetTag; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.transaction.annotation.Transactional; + +/** + * {@link TargetTag} repository. + * + * + * + */ +@Transactional(readOnly = true) +public interface DistributionSetTagRepository extends BaseEntityRepository { + /** + * deletes the {@link DistributionSet} with the given name. + * + * @param tagName + * to be deleted + * @return 1 if tag was deleted + */ + @Modifying + @Transactional + Long deleteByName(final String tagName); + + /** + * find {@link DistributionSetTag} by its name. + * + * @param tagName + * to filter on + * @return the {@link DistributionSetTag} if found, otherwise null + */ + DistributionSetTag findByNameEquals(final String tagName); + + /** + * Returns all instances of the type. + * + * @return all entities + */ + @Override + List findAll(); +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTypeRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTypeRepository.java new file mode 100755 index 000000000..b9da6b3cc --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTypeRepository.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.transaction.annotation.Transactional; + +/** + * {@link PagingAndSortingRepository} for {@link DistributionSetType}. + * + * + * + * + */ +@Transactional(readOnly = true) +public interface DistributionSetTypeRepository + extends BaseEntityRepository, JpaSpecificationExecutor { + + /** + * + * @param pageable + * page parameters + * @param isDeleted + * to true if only soft deleted entries of + * false if undeleted ones + * @return list of found {@link DistributionSetType}s + */ + Page findByDeleted(Pageable pageable, boolean isDeleted); + + /** + * @param isDeleted + * to true if only marked as deleted have to be + * count or all undeleted. + * @return number of {@link DistributionSetType}s in the repository. + */ + Long countByDeleted(boolean isDeleted); + + /** + * Counts all distribution set type where a specific software module type is + * assigned to. + * + * @param softwareModuleType + * the software module type to count the distribution set type + * which has this software module type assigned + * @return the number of {@link DistributionSetType}s in the repository + * assigned to the given software module type + */ + Long countByElementsSmType(SoftwareModuleType softwareModuleType); +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/EclipseLinkTargetInfoRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/EclipseLinkTargetInfoRepository.java new file mode 100755 index 000000000..c9be0bf38 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/EclipseLinkTargetInfoRepository.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.Collection; +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import javax.transaction.Transactional; + +import org.eclipse.hawkbit.repository.model.TargetInfo; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.stereotype.Service; + +/** + * Custom repository implementation as standard spring repository fails as of + * https://bugs.eclipse.org/bugs/show_bug.cgi?id=415027 . + * + * + * + */ +@Service +@Transactional +public class EclipseLinkTargetInfoRepository implements TargetInfoRepository { + + @Autowired + private EntityManager entityManager; + + @Override + @Modifying + public void setTargetUpdateStatus(final TargetUpdateStatus status, final List targets) { + final Query query = entityManager.createQuery( + "update TargetInfo ti set ti.updateStatus = :status where ti.targetId in :targets and ti.updateStatus != :status"); + query.setParameter("targets", targets); + query.setParameter("status", status); + + } + + @Override + @Modifying + @CacheEvict(value = { "targetStatus", "distributionUsageInstalled", "targetsLastPoll" }, allEntries = true) + public S save(final S entity) { + + if (entity.isNew()) { + entityManager.persist(entity); + return entity; + } else { + return entityManager.merge(entity); + } + } + + @Override + @Modifying + @CacheEvict(value = { "targetStatus", "distributionUsageInstalled", "targetsLastPoll" }, allEntries = true) + public void deleteByTargetIdIn(final Collection targetIDs) { + final javax.persistence.Query query = entityManager + .createQuery("DELETE FROM TargetInfo ti where ti.targetId IN :target"); + query.setParameter("target", targetIDs); + + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ExternalArtifactProviderRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ExternalArtifactProviderRepository.java new file mode 100755 index 000000000..05634ef63 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ExternalArtifactProviderRepository.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import org.eclipse.hawkbit.repository.model.ExternalArtifactProvider; +import org.springframework.transaction.annotation.Transactional; + +/** + * Repository for {@link ExternalArtifactProvider}. + * + * + * + * + */ +@Transactional(readOnly = true) +public interface ExternalArtifactProviderRepository extends BaseEntityRepository { + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ExternalArtifactRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ExternalArtifactRepository.java new file mode 100755 index 000000000..c7ece2445 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ExternalArtifactRepository.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import org.eclipse.hawkbit.repository.model.ExternalArtifact; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.transaction.annotation.Transactional; + +/** + * {@link ExternalArtifact} repository. + * + * + * + */ +@Transactional(readOnly = true) +public interface ExternalArtifactRepository extends BaseEntityRepository { + + /** + * Searches for external artifact for a base software module. + * + * @param pageReq + * Pageable + * @param swId + * software module id + * + * @return Page + */ + Page findBySoftwareModuleId(Pageable pageReq, final Long swId); + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/LocalArtifactRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/LocalArtifactRepository.java new file mode 100755 index 000000000..763b6e5a2 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/LocalArtifactRepository.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.List; + +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.transaction.annotation.Transactional; + +/** + * {@link LocalArtifact} repository. + * + * + * + */ +@Transactional(readOnly = true) +public interface LocalArtifactRepository extends BaseEntityRepository { + /** + * Searches for a {@link LocalArtifact} based on given gridFsFileName. + * + * @param gridFsFileName + * to search + * @return list of {@link LocalArtifact}s. + */ + List findByGridFsFileName(String gridFsFileName); + + /** + * Searches for a {@link LocalArtifact} based on given gridFsFileName. + * + * @param gridFsFileName + * to search + * @return {@link LocalArtifact} the first in the result list + */ + LocalArtifact findFirstByGridFsFileName(String gridFsFileName); + + /** + * Searches for a {@link LocalArtifact} based user provided filename at + * upload. + * + * @param filename + * to search + * @return list of {@link LocalArtifact}. + */ + List findByFilename(String filename); + + /** + * Searches for local artifact for a base software module. + * + * @param pageReq + * Pageable + * @param swId + * software module id + * + * @return Page + */ + Page findBySoftwareModuleId(Pageable pageReq, final Long swId); + + /** + * Searches for a {@link LocalArtifact} based user provided filename at + * upload and selected software module id. + * + * @param filename + * to search + * @param softwareModuleId + * selected software module id + * @return list of {@link LocalArtifact}. + */ + List findByFilenameAndSoftwareModuleId(final String filename, final Long softwareModuleId); + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/NoCountPagingRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/NoCountPagingRepository.java new file mode 100755 index 000000000..4290607fd --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/NoCountPagingRepository.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.io.Serializable; +import java.util.List; + +import javax.persistence.Entity; +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.support.SimpleJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * Workaround as spring data does not provide a {@link Slice} based + * {@link JpaRepository#findAll()}. + * + * + * + * + */ +@Repository +public class NoCountPagingRepository { + + @Autowired + protected EntityManager em; + + /** + * Searches without the need for an extra count query. + * + * @param spec + * to search for + * @param pageable + * information + * @param domainClass + * of the {@link Entity} + * + * @return {@link Slice} of data + * + * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll(org.springframework + * .data.jpa.domain.Specification, + * org.springframework.data.domain.Pageable) + */ + public Slice findAll(final Specification spec, final Pageable pageable, + final Class domainClass) { + final SimpleJpaNoCountRepository noCountDao = new SimpleJpaNoCountRepository(domainClass, em); + return noCountDao.findAll(spec, pageable); + } + + /** + * Searches without the need for an extra count query. + * + * @param pageable + * information + * @param domainClass + * of the {@link Entity} + * + * @return {@link Slice} of data + * + * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll(org.springframework + * .data.jpa.domain.Specification, + * org.springframework.data.domain.Pageable) + */ + public Slice findAll(final Pageable pageable, final Class domainClass) { + final SimpleJpaNoCountRepository noCountDao = new SimpleJpaNoCountRepository(domainClass, em); + return noCountDao.findAll(pageable); + } + + /** + * Repository implementation with disabled count query. + * + * + * + * @param + * entity type + * @param + * key or ID type + */ + public static class SimpleJpaNoCountRepository extends SimpleJpaRepository { + + /** + * Constructor. + * + * @param domainClass + * of the {@link Entity} + * @param em + * {@link EntityManager} instance for the queries + */ + public SimpleJpaNoCountRepository(final Class domainClass, final EntityManager em) { + super(domainClass, em); + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.support.SimpleJpaRepository# + * readPage(javax.persistence .TypedQuery, + * org.springframework.data.domain.Pageable, + * org.springframework.data.jpa.domain.Specification) + */ + @Override + protected Page readPage(final TypedQuery query, final Pageable pageable, final Specification spec) { + query.setFirstResult(pageable.getOffset()); + query.setMaxResults(pageable.getPageSize()); + + final List content = query.getResultList(); + + return new PageImpl(content, pageable, content.size()); + } + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ReportManagement.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ReportManagement.java new file mode 100755 index 000000000..801f6c80d --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/ReportManagement.java @@ -0,0 +1,631 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Expression; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.JoinType; +import javax.persistence.criteria.ListJoin; +import javax.persistence.criteria.Root; + +import org.eclipse.hawkbit.report.model.DataReportSeries; +import org.eclipse.hawkbit.report.model.DataReportSeriesItem; +import org.eclipse.hawkbit.report.model.InnerOuterDataReportSeries; +import org.eclipse.hawkbit.report.model.ListReportSeries; +import org.eclipse.hawkbit.report.model.SeriesTime; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSet_; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetInfo; +import org.eclipse.hawkbit.repository.model.TargetInfo_; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.eclipse.hawkbit.repository.model.Target_; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * Service layer for generating SP reportings. + * + * + * + * + */ +@Transactional(readOnly = true) +@Validated +@Service +@ConfigurationProperties +public class ReportManagement { + + @Value("${spring.jpa.database}") + private String databaseType; + + private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM"); + + private static final String H2_TARGET_CREATED_SQL_TEMPLATE = "SELECT TO_CHAR( DATEADD('second', target0_.created_at / 1000, DATE '1970-01-01'), '%s') AS col_0_0_, count(target0_.controller_id) AS col_1_0_ from sp_target target0_ WHERE TO_CHAR(DATEADD('second', target0_.created_at / 1000, DATE '1970-01-01'),'%s') BETWEEN TO_CHAR('%s', '%s') and TO_CHAR('%s', '%s') AND UPPER(target0_.tenant)=UPPER('%s') GROUP BY TO_CHAR(DATEADD('second', target0_.created_at / 1000, DATE '1970-01-01'), '%s')"; + + private static final String H2_CONTROLLER_FRRDBACK_SQL_TEMPLATE = "SELECT TO_CHAR(DATEADD('second', action_.created_at / 1000, DATE '1970-01-01'), '%s') AS col_0_0_, count(action_.id) AS col_1_0_ FROM sp_action action_ WHERE TO_CHAR(DATEADD('second', action_.created_at / 1000, DATE '1970-01-01'), '%s') BETWEEN TO_CHAR('%s', '%s') AND TO_CHAR('%s', '%s') AND UPPER(action_.tenant)=UPPER('%s') GROUP BY TO_CHAR(DATEADD('second', action_.created_at / 1000, DATE '1970-01-01'), '%s')"; + + private static final String MYSQL_TARGET_CREATED_SQL_TEMPLATE = "SELECT DATE_FORMAT(FROM_UNIXTIME(target0_.created_at / 1000), '%s') AS col_0_0_, COUNT(target0_.controller_id) AS col_1_0_ FROM sp_target target0_ WHERE DATE_FORMAT(FROM_UNIXTIME(target0_.created_at / 1000),'%s') BETWEEN DATE_FORMAT('%s', '%s') AND DATE_FORMAT('%s', '%s') AND UPPER(target0_.tenant)=UPPER('%s') GROUP BY DATE_FORMAT(FROM_UNIXTIME(target0_.created_at / 1000), '%s')"; + + private static final String MYSQL_CONTROLLER_FRRDBACK_SQL_TEMPLATE = "SELECT DATE_FORMAT(FROM_UNIXTIME(action_.created_at / 1000), '%s') AS col_0_0_, COUNT(action_.id) as col_1_0_ FROM sp_action action_ WHERE DATE_FORMAT(FROM_UNIXTIME(action_.created_at / 1000),'%s') BETWEEN DATE_FORMAT('%s', '%s') AND DATE_FORMAT('%s', '%s') AND UPPER(action_.tenant)=UPPER('%s') GROUP BY DATE_FORMAT(FROM_UNIXTIME(action_.created_at / 1000), '%s')"; + + private static final String MYSQL_DB_TYPE = "MYSQL"; + + private static final String H2_DB_TYPE = "H2"; + + @Autowired + private EntityManager entityManager; + + @Autowired + private TenantAware tenantAware; + + /** + * Generates a report of all targets of their current update status count. + * For each {@link TargetUpdateStatus} an total count of targets which are + * in this status currently. + * + * @return a data report series which contains the target count for each + * target update status + */ + @Cacheable("targetStatus") + public DataReportSeries targetStatus() { + + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + final CriteriaQuery query = cb.createQuery(Object[].class); + final Root targetRoot = query.from(Target.class); + final Join targetInfo = targetRoot.join(Target_.targetInfo); + final Expression countColumn = cb.count(targetInfo.get(TargetInfo_.targetId)); + final CriteriaQuery multiselect = query + .multiselect(targetInfo.get(TargetInfo_.updateStatus), countColumn) + .groupBy(targetInfo.get(TargetInfo_.updateStatus)) + .orderBy(cb.desc(targetInfo.get(TargetInfo_.updateStatus))); + + // | col1 | col2 | + // | U_STATUS | COUNT | + final List resultList = entityManager.createQuery(multiselect).getResultList(); + + final List> reportSeriesItems = resultList.stream() + .map(r -> new DataReportSeriesItem((TargetUpdateStatus) r[0], (Long) r[1])) + .collect(Collectors.toList()); + + return new DataReportSeries<>("Target Status Overview", reportSeriesItems); + } + + /** + * Generates a report of the top x distribution set assigned usage as a list + * of {@link InnerOuterDataReportSeries} which is ideal for generate a donut + * chart out of it. The inner series contains the distribution set names and + * total count usage. The outer series contains each version usage and its + * usage count. {@code inner: ds1:5 -> outer: vers 0.0.0:3, vers 1.0.0:2} + * {@code inner: ds2:1 -> outer: vers 0.0.1:1} + * + * The top x entries are seperated within the series, the rest of the + * distribution sets usage are summarized to a "misc" series. + * + * @param topXEntries + * the top entries which should be shown, the rest distribution + * set entries are summarized as "misc" + * @return a list of inner and outer series of distribution set usage + */ + @Cacheable("distributionUsageAssigned") + public List> distributionUsageAssigned(final int topXEntries) { + + // top X entries distribution usage + final CriteriaBuilder cbTopX = entityManager.getCriteriaBuilder(); + final CriteriaQuery queryTopX = cbTopX.createQuery(Object[].class); + final Root rootTopX = queryTopX.from(DistributionSet.class); + final ListJoin joinTopX = rootTopX.join(DistributionSet_.assignedToTargets, + JoinType.LEFT); + final Expression countColumn = cbTopX.count(joinTopX); + // top x usage query + final CriteriaQuery groupBy = queryTopX + .multiselect(rootTopX.get(DistributionSet_.name), rootTopX.get(DistributionSet_.version), countColumn) + .where(cbTopX.equal(rootTopX.get(DistributionSet_.deleted), false)) + .groupBy(rootTopX.get(DistributionSet_.name), rootTopX.get(DistributionSet_.version)) + .orderBy(cbTopX.desc(countColumn), cbTopX.asc(rootTopX.get(DistributionSet_.name))); + // | col1 | col2 | col3 | + // | NAME | VER | COUNT | + final List resultListTop = entityManager.createQuery(groupBy).getResultList(); + // end of top X entries distribution usage + + return mapDistirbutionUsageResultToDataReport(topXEntries, resultListTop); + } + + /** + * Generates a report of the top x distribution set installed usage as a + * list of {@link InnerOuterDataReportSeries} which is ideal for generate a + * donut chart out of it. The inner series contains the distribution set + * names and total count usage. The outer series contains each version usage + * and its usage count. + * {@code inner: ds1:5 -> outer: vers 0.0.0:3, vers 1.0.0:2} + * {@code inner: ds2:1 -> outer: vers 0.0.1:1} + * + * The top x entries are seperated within the series, the rest of the + * distribution sets usage are summarized to a "misc" series. + * + * @param topXEntries + * the top entries which should be shown, the rest distribution + * set entries are summarized as "misc" + * @return a list of inner and outer series of distribution set usage + */ + @Cacheable("distributionUsageInstalled") + public List> distributionUsageInstalled(final int topXEntries) { + // top X entries distribution usage + final CriteriaBuilder cbTopX = entityManager.getCriteriaBuilder(); + final CriteriaQuery queryTopX = cbTopX.createQuery(Object[].class); + final Root rootTopX = queryTopX.from(DistributionSet.class); + final ListJoin joinTopX = rootTopX.join(DistributionSet_.installedAtTargets, + JoinType.LEFT); + final Expression countColumn = cbTopX.count(joinTopX); + // top x usage query + final CriteriaQuery groupBy = queryTopX + .multiselect(rootTopX.get(DistributionSet_.name), rootTopX.get(DistributionSet_.version), countColumn) + .where(cbTopX.equal(rootTopX.get(DistributionSet_.deleted), false)) + .groupBy(rootTopX.get(DistributionSet_.name), rootTopX.get(DistributionSet_.version)) + .orderBy(cbTopX.desc(countColumn), cbTopX.asc(rootTopX.get(DistributionSet_.name))); + // | col1 | col2 | col3 | + // | NAME | VER | COUNT | + final List resultListTop = entityManager.createQuery(groupBy).getResultList(); + // end of top X entries distribution usage + + return mapDistirbutionUsageResultToDataReport(topXEntries, resultListTop); + } + + /** + * Generates report for target created over period. + * + * @param dateType + * {@link PerMonth} + * @param from + * start date + * @param to + * end date + * @return DataReportSeries ListReportSeries list of target created + * count + */ + @Cacheable("targetsCreatedOverPeriod") + public DataReportSeries targetsCreatedOverPeriod(final DateType dateType, final LocalDateTime from, + final LocalDateTime to) { + final Query createNativeQuery = entityManager + .createNativeQuery(getTargetsCreatedQueryTemplate(dateType, from, to)); + final List resultList = createNativeQuery.getResultList(); + + final List> reportItems = resultList.stream() + .map(r -> new DataReportSeriesItem<>(dateType.format((String) r[0]), ((Number) r[1]).longValue())) + .collect(Collectors.toList()); + + final DataReportSeries report = new DataReportSeries<>("CreatedTargets", reportItems); + return report; + } + + private String getTargetsCreatedQueryTemplate(final DateType dateType, final LocalDateTime from, + final LocalDateTime to) { + switch (databaseType) { + case H2_DB_TYPE: + return String.format(H2_TARGET_CREATED_SQL_TEMPLATE, dateTimeFormatToSqlFormat(dateType), + dateTimeFormatToSqlFormat(dateType), from.format(DATE_FORMAT), dateTimeFormatToSqlFormat(dateType), + to.format(DATE_FORMAT), dateTimeFormatToSqlFormat(dateType), tenantAware.getCurrentTenant(), + dateTimeFormatToSqlFormat(dateType)); + case MYSQL_DB_TYPE: + return String.format(MYSQL_TARGET_CREATED_SQL_TEMPLATE, dateTimeFormatToSqlFormat(dateType), + dateTimeFormatToSqlFormat(dateType), from.toString(), dateTimeFormatToSqlFormat(dateType), + to.toString(), dateTimeFormatToSqlFormat(dateType), tenantAware.getCurrentTenant(), + dateTimeFormatToSqlFormat(dateType)); + default: + return null; + } + } + + private String getFeedbackReceivedQueryTemplate(final DateType dateType, final LocalDateTime from, + final LocalDateTime to) { + switch (databaseType) { + case H2_DB_TYPE: + return String.format(H2_CONTROLLER_FRRDBACK_SQL_TEMPLATE, dateTimeFormatToSqlFormat(dateType), + dateTimeFormatToSqlFormat(dateType), from.format(DATE_FORMAT), dateTimeFormatToSqlFormat(dateType), + to.format(DATE_FORMAT), dateTimeFormatToSqlFormat(dateType), tenantAware.getCurrentTenant(), + dateTimeFormatToSqlFormat(dateType)); + case MYSQL_DB_TYPE: + return String.format(MYSQL_CONTROLLER_FRRDBACK_SQL_TEMPLATE, dateTimeFormatToSqlFormat(dateType), + dateTimeFormatToSqlFormat(dateType), from.toString(), dateTimeFormatToSqlFormat(dateType), + to.toString(), dateTimeFormatToSqlFormat(dateType), tenantAware.getCurrentTenant(), + dateTimeFormatToSqlFormat(dateType)); + default: + return null; + } + } + + /** + * Generates report for feedback over period. + * + * @param dateType + * {@link PerMonth} + * @param from + * start date + * @param to + * end date + * @return DataReportSeries ListReportSeries list of action status + * count + */ + @Cacheable("feedbackReceivedOverTime") + public DataReportSeries feedbackReceivedOverTime(final DateType dateType, final LocalDateTime from, + final LocalDateTime to) { + final Query createNativeQuery = entityManager + .createNativeQuery(getFeedbackReceivedQueryTemplate(dateType, from, to)); + final List resultList = createNativeQuery.getResultList(); + + final List> reportItems = resultList.stream() + .map(r -> new DataReportSeriesItem<>(dateType.format((String) r[0]), ((Number) r[1]).longValue())) + .collect(Collectors.toList()); + + final DataReportSeries report = new DataReportSeries<>("FeedbackRecieved", reportItems); + return report; + } + + /** + * Generates a report as a {@link ListReportSeries} targets polled based on + * the {@link TargetStatus#getLastTargetQuery()} within an hour, day, week, + * month, year, more than a year, never. + * + * The order of the numbers within the {@link DataReportSeries} is the order + * hour, day, week, month, year, more than a year, never. + * + * @return a {@link DataReportSeries} which contains the number of targets + * which have not been polled in the last hour, day, ... year,more + * than a year, never. + * + */ + @Cacheable("targetsLastPoll") + public DataReportSeries targetsLastPoll() { + + final LocalDateTime now = LocalDateTime.now(); + final LocalDateTime beforeHour = now.minusHours(1); + final LocalDateTime beforeDay = now.minusDays(1); + final LocalDateTime beforeWeek = now.minusWeeks(1); + final LocalDateTime beforeMonth = now.minusMonths(1); + final LocalDateTime beforeYear = now.minusYears(1); + + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + final List> resultList = new ArrayList<>(); + + // hours + resultList.add(new DataReportSeriesItem(SeriesTime.HOUR, + entityManager.createQuery(createCountSelectTargetsLastPoll(cb, beforeHour, now)).getSingleResult())); + // days + resultList.add(new DataReportSeriesItem(SeriesTime.DAY, entityManager + .createQuery(createCountSelectTargetsLastPoll(cb, beforeDay, beforeHour)).getSingleResult())); + // weeks + resultList.add(new DataReportSeriesItem(SeriesTime.WEEK, entityManager + .createQuery(createCountSelectTargetsLastPoll(cb, beforeWeek, beforeDay)).getSingleResult())); + // months + resultList.add(new DataReportSeriesItem(SeriesTime.MONTH, entityManager + .createQuery(createCountSelectTargetsLastPoll(cb, beforeMonth, beforeWeek)).getSingleResult())); + // years + resultList.add(new DataReportSeriesItem(SeriesTime.YEAR, entityManager + .createQuery(createCountSelectTargetsLastPoll(cb, beforeYear, beforeMonth)).getSingleResult())); + // years + resultList.add(new DataReportSeriesItem(SeriesTime.MORE_THAN_YEAR, + entityManager.createQuery(createCountSelectTargetsLastPoll(cb, null, beforeYear)).getSingleResult())); + // never + resultList.add(new DataReportSeriesItem(SeriesTime.NEVER, + entityManager.createQuery(createCountSelectTargetsLastPoll(cb, null, null)).getSingleResult())); + + return new DataReportSeries<>("TargetLastPoll", resultList); + } + + private CriteriaQuery createCountSelectTargetsLastPoll(final CriteriaBuilder cb, final LocalDateTime from, + final LocalDateTime to) { + + Long start = null; + Long end = null; + if (from != null) { + start = from.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } + if (to != null) { + end = to.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } + + // count select statement + final CriteriaQuery countSelect = cb.createQuery(Long.class); + final Root countSelectRoot = countSelect.from(Target.class); + final Join targetInfoJoin = countSelectRoot.join(Target_.targetInfo); + countSelect.select(cb.count(countSelectRoot)); + if (start != null && end != null) { + countSelect.where(cb.between(targetInfoJoin.get(TargetInfo_.lastTargetQuery), start, end)); + } else if (from == null && to != null) { + countSelect.where(cb.lessThanOrEqualTo(targetInfoJoin.get(TargetInfo_.lastTargetQuery), end)); + } else { + countSelect.where(cb.isNull(targetInfoJoin.get(TargetInfo_.lastTargetQuery))); + } + return countSelect; + } + + private List> mapDistirbutionUsageResultToDataReport(final int topXEntries, + final List resultListTop) { + final List> innerOuterReport = new ArrayList<>(); + final Map map = new LinkedHashMap<>(); + + int topXCounter = 0; + + for (final Object[] objects : resultListTop) { + + final boolean containsInnerOuter = map.containsKey(new DSName((String) objects[0])); + final String name = containsInnerOuter || topXCounter < topXEntries ? (String) objects[0] : null; + final DSName dsName = new DSName(name); + final String version = containsInnerOuter || topXCounter < topXEntries ? (String) objects[1] : null; + final Long count = (Long) objects[2]; + + InnerOuter innerouter = map.get(dsName); + if (innerouter == null) { + topXCounter++; + innerouter = new InnerOuter(dsName); + map.put(new DSName(name), innerouter); + } + innerouter.addOuter(new DSName(version), count); + } + + for (final InnerOuter inner : map.values()) { + final List> outerReportItems = new ArrayList<>(); + + if (inner.name.getName() != null) { + for (final InnerOuter outer : inner.outer) { + outerReportItems.add(outer.toItem()); + } + } else { + outerReportItems.add(new DataReportSeriesItem("misc", inner.count)); + } + + innerOuterReport.add(new InnerOuterDataReportSeries( + new DataReportSeries<>("DS-Name", Collections.singletonList(inner.toItem())), + new DataReportSeries<>("DS-Version", outerReportItems))); + } + + return innerOuterReport; + } + + private final class InnerOuter { + final DSName name; + long count; + final List outer; + + private InnerOuter(final DSName idName) { + name = idName; + outer = new ArrayList<>(); + } + + private InnerOuter(final DSName idName, final long count) { + name = idName; + this.count = count; + outer = new ArrayList<>(); + } + + private void addOuter(final DSName idName, final long count) { + outer.add(new InnerOuter(idName, count)); + this.count += count; + } + + private DataReportSeriesItem toItem() { + return new DataReportSeriesItem(name.getName() != null ? name.getName() : "misc", count); + } + } + + /** + * Object contains the name and the id of an entity. + * + * + * + * + */ + private static final class DSName { + + private final String name; + + /** + * @param id + * the ID of an entity + * @param name + * the name of an entity + */ + private DSName(final String name) { + this.name = name; + } + + /** + * @return the name + */ + private String getName() { + return name; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (name == null ? 0 : name.hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { // NOSONAR - as this is + // generated + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final DSName other = (DSName) obj; + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + return true; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "DSName [name=" + name + "]"; + } + } + + private String dateTimeFormatToSqlFormat(final DateType datatype) { + switch (databaseType) { + case H2_DB_TYPE: + return datatype.h2Format(); + case MYSQL_DB_TYPE: + return datatype.mySqlFormat(); + default: + return null; + } + } + + /** + * Return DateTypes. + * + * + * + */ + public static final class DateTypes implements Serializable { + /** + * + */ + private static final long serialVersionUID = 1L; + private static final PerMonth PER_MONTH = new PerMonth(); + + private DateTypes() { + + } + + /** + * @return PerMonth + */ + public static PerMonth perMonth() { + return PER_MONTH; + } + } + + /** + * Data base format. + * + * + * + * @param + */ + public interface DateType { + /** + * @param s + * @return T + */ + T format(String s); + + /** + * h2 format. + * + * @return String + */ + String h2Format(); + + /** + * mysql format. + * + * @return String + */ + String mySqlFormat(); + } + + /** + * Gives the date format based on DB H2 or mySql. + * + * + * + */ + public static final class PerMonth implements DateType, Serializable { + private static final long serialVersionUID = 1L; + private static final String DATE_PATTERN = "yyyy-MM"; + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.repository.ReportManagement.DateType# + * format(java. lang.String) + */ + @Override + public LocalDate format(final String s) { + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_PATTERN); + final YearMonth ym = YearMonth.parse(s, formatter); + return ym.atDay(1); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.repository.ReportManagement.DateType# + * h2Format() + */ + @Override + public String h2Format() { + return DATE_PATTERN; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.repository.ReportManagement.DateType# + * mySqlFormat( ) + */ + @Override + public String mySqlFormat() { + return "%Y-%m"; + } + + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/SoftwareManagement.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/SoftwareManagement.java new file mode 100755 index 000000000..98cb1f418 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/SoftwareManagement.java @@ -0,0 +1,944 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.persistence.Entity; +import javax.persistence.EntityManager; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.ListJoin; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.CustomSoftwareModule; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSet_; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata; +import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata_; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.repository.model.SoftwareModule_; +import org.eclipse.hawkbit.repository.model.SwMetadataCompositeKey; +import org.eclipse.hawkbit.repository.specifications.SoftwareModuleSpecification; +import org.hibernate.validator.constraints.NotEmpty; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.domain.Specifications; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import com.google.common.base.Strings; +import com.google.common.collect.Sets; + +/** + * Business facade for managing the deployable {@link SoftwareModule}s. + * + * + * + * + * + */ +@Transactional(readOnly = true) +@Validated +@Service +public class SoftwareManagement { + + @Autowired + private EntityManager entityManager; + + @Autowired + private DistributionSetRepository distributionSetRepository; + + @Autowired + private DistributionSetTypeRepository distributionSetTypeRepository; + + @Autowired + private SoftwareModuleRepository softwareModuleRepository; + + @Autowired + private SoftwareModuleMetadataRepository softwareModuleMetadataRepository; + + @Autowired + private SoftwareModuleTypeRepository softwareModuleTypeRepository; + + @Autowired + private NoCountPagingRepository criteriaNoCountDao; + + @Autowired + private AuditorAware auditorProvider; + + @Autowired + private ArtifactManagement artifactManagement; + + /** + * Updates existing {@link SoftwareModule}. Updateable values are + * {@link SoftwareModule#getDescription()} + * {@link SoftwareModule#getVendor()}. + * + * @param sm + * to update + * + * @return the saved {@link Entity}. + * + * @throws NullPointerException + * of {@link SoftwareModule#getId()} is null + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public SoftwareModule updateSoftwareModule(@NotNull final SoftwareModule sm) { + checkNotNull(sm.getId()); + + final SoftwareModule module = softwareModuleRepository.findOne(sm.getId()); + + if (null == sm.getDescription() || !sm.getDescription().equals(module.getDescription())) { + module.setDescription(sm.getDescription()); + } + if (null == sm.getVendor() || !sm.getVendor().equals(module.getVendor())) { + module.setVendor(sm.getVendor()); + } + return softwareModuleRepository.save(module); + } + + /** + * Updates existing {@link SoftwareModuleType}. Updatable value is + * {@link SoftwareModuleType#getDescription()} and + * {@link SoftwareModuleType#getColour()}. + * + * @param sm + * to update + * @return updated {@link Entity} + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public SoftwareModuleType updateSoftwareModuleType(@NotNull final SoftwareModuleType sm) { + checkNotNull(sm.getId()); + + final SoftwareModuleType type = softwareModuleTypeRepository.findOne(sm.getId()); + + if (sm.getDescription() != null && !sm.getDescription().equals(type.getDescription())) { + type.setDescription(sm.getDescription()); + } + if (sm.getColour() != null && !sm.getColour().equals(type.getColour())) { + type.setColour(sm.getColour()); + } + return softwareModuleTypeRepository.save(type); + } + + /** + * + * @param swModule + * SoftwareModule to create + * @return SoftwareModule + * @throws EntityAlreadyExistsException + * if a given entity already exists + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_REPOSITORY) + public SoftwareModule createSoftwareModule(@NotNull final SoftwareModule swModule) { + if (null != swModule.getId()) { + throw new EntityAlreadyExistsException(); + } + return softwareModuleRepository.save(swModule); + } + + /** + * Create {@link SoftwareModule}s in the repository. + * + * @param swModules + * {@link SoftwareModule}s to create + * @return SoftwareModule + * @throws EntityAlreadyExistsException + * if a given entity already exists + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_REPOSITORY) + public List createSoftwareModule(@NotNull final Iterable swModules) { + swModules.forEach(swModule -> { + if (null != swModule.getId()) { + throw new EntityAlreadyExistsException(); + } + }); + + return softwareModuleRepository.save(swModules); + + } + + /** + * retrieves the {@link SoftwareModule}s by their {@link SoftwareModuleType} + * . + * + * @param pageable + * page parameters + * @param type + * to be filtered on + * @return the found {@link SoftwareModule}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Slice findSoftwareModulesByType(@NotNull final Pageable pageable, + @NotNull final SoftwareModuleType type) { + + final List> specList = new ArrayList>(); + + Specification spec = SoftwareModuleSpecification.equalType(type); + specList.add(spec); + + spec = SoftwareModuleSpecification.isDeletedFalse(); + specList.add(spec); + + return findSwModuleByCriteriaAPI(pageable, specList); + } + + /** + * Counts {@link SoftwareModule}s with given {@link SoftwareModuleType}. + * + * @param type + * to count + * @return number of found {@link SoftwareModule}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Long countSoftwareModulesByType(@NotNull final SoftwareModuleType type) { + + final List> specList = new ArrayList>(); + + Specification spec = SoftwareModuleSpecification.equalType(type); + specList.add(spec); + + spec = SoftwareModuleSpecification.isDeletedFalse(); + specList.add(spec); + + return countSwModuleByCriteriaAPI(specList); + } + + /** + * Finds {@link SoftwareModule} by given id. + * + * @param id + * to search for + * @return the found {@link SoftwareModule}s or null if not + * found. + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY + SpringEvalExpressions.HAS_AUTH_OR + + SpringEvalExpressions.IS_CONTROLLER) + public SoftwareModule findSoftwareModuleById(@NotNull final Long id) { + return artifactManagement.findSoftwareModuleById(id); + } + + /** + * retrieves {@link SoftwareModule}s by their name AND version. + * + * @param name + * of the {@link SoftwareModule} + * @param version + * of the {@link SoftwareModule} + * @return the found {@link SoftwareModule}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public List findSoftwareModuleByNameAndVersion(@NotEmpty final String name, + @NotEmpty final String version) { + + return softwareModuleRepository.findByNameAndVersion(name, version); + } + + /** + * Deletes the given {@link SoftwareModule} {@link Entity}. + * + * @param bsm + * is the {@link SoftwareModule} to be deleted + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_DELETE_REPOSITORY) + public void deleteSoftwareModule(@NotNull final SoftwareModule bsm) { + + deleteSoftwareModules(Sets.newHashSet(bsm.getId())); + } + + private boolean isUnassigned(final SoftwareModule bsmMerged) { + return distributionSetRepository.findByModules(bsmMerged).isEmpty(); + } + + private Slice findSwModuleByCriteriaAPI(@NotNull final Pageable pageable, + @NotEmpty final List> specList) { + + Specifications specs = Specifications.where(specList.get(0)); + if (specList.size() > 1) { + for (final Specification s : specList.subList(1, specList.size())) { + specs = specs.and(s); + } + } + + return criteriaNoCountDao.findAll(specs, pageable, SoftwareModule.class); + } + + private Long countSwModuleByCriteriaAPI(@NotEmpty final List> specList) { + Specifications specs = Specifications.where(specList.get(0)); + if (specList.size() > 1) { + for (final Specification s : specList.subList(1, specList.size())) { + specs = specs.and(s); + } + } + + return softwareModuleRepository.count(specs); + } + + private void deleteGridFsArtifacts(final SoftwareModule swModule) { + for (final LocalArtifact localArtifact : swModule.getLocalArtifacts()) { + artifactManagement.deleteGridFsArtifact(localArtifact); + } + } + + /** + * Deletes {@link SoftwareModule}s which is any if the given ids. + * + * @param ids + * of the Software Moduels to be deleted + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_DELETE_REPOSITORY) + public void deleteSoftwareModules(@NotNull final Iterable ids) { + final List swModulesToDelete = softwareModuleRepository.findByIdIn(ids); + final Set assignedModuleIds = new HashSet<>(); + swModulesToDelete.forEach(swModule -> { + + // delete binary data of artifacts + deleteGridFsArtifacts(swModule); + + if (isUnassigned(swModule)) { + + softwareModuleRepository.delete(swModule); + + } else { + + assignedModuleIds.add(swModule.getId()); + } + }); + + if (!assignedModuleIds.isEmpty()) { + String currentUser = null; + if (auditorProvider != null) { + currentUser = auditorProvider.getCurrentAuditor(); + } + softwareModuleRepository.deleteSoftwareModule(System.currentTimeMillis(), currentUser, + assignedModuleIds.toArray(new Long[0])); + } + } + + /** + * Retrieves all software modules. Deleted ones are filtered. + * + * @param pageable + * pagination parameter + * @return the found {@link SoftwareModule}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Slice findSoftwareModulesAll(@NotNull final Pageable pageable) { + + final List> specList = new ArrayList>(); + + Specification spec = SoftwareModuleSpecification.isDeletedFalse(); + specList.add(spec); + + spec = new Specification() { + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, + final CriteriaBuilder cb) { + if (!query.getResultType().isAssignableFrom(Long.class)) { + root.fetch(SoftwareModule_.type); + } + return cb.conjunction(); + } + }; + + specList.add(spec); + + return findSwModuleByCriteriaAPI(pageable, specList); + } + + /** + * Count all {@link SoftwareModule}s in the repository that are not marked + * as deleted. + * + * @return number of {@link SoftwareModule}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Long countSoftwareModulesAll() { + + final List> specList = new ArrayList>(); + + final Specification spec = SoftwareModuleSpecification.isDeletedFalse(); + specList.add(spec); + + return countSwModuleByCriteriaAPI(specList); + } + + /** + * Retrieves software module including details ( + * {@link SoftwareModule#getArtifacts()}). + * + * @param id + * parameter + * @param isDeleted + * parameter + * @return the found {@link SoftwareModule}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public SoftwareModule findSoftwareModuleWithDetails(@NotNull final Long id) { + return artifactManagement.findSoftwareModuleWithDetails(id); + } + + /** + * Retrieves all {@link SoftwareModule}s with a given specification. + * + * @param spec + * the specification to filter the software modules + * @param pageable + * pagination parameter + * @return the found {@link SoftwareModule}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Page findSoftwareModulesByPredicate(@NotNull final Specification spec, + @NotNull final Pageable pageable) { + return softwareModuleRepository.findAll(spec, pageable); + } + + /** + * Retrieves all {@link SoftwareModuleType}s with a given specification. + * + * @param spec + * the specification to filter the software modules types + * @param pageable + * pagination parameter + * @return the found {@link SoftwareModuleType}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Page findSoftwareModuleTypesByPredicate( + @NotNull final Specification spec, @NotNull final Pageable pageable) { + return softwareModuleTypeRepository.findAll(spec, pageable); + } + + /** + * Retrieves all software modules with a given list of ids + * {@link SoftwareModule#getId()}. + * + * @param ids + * to search for + * @return {@link List} of found {@link SoftwareModule}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public List findSoftwareModulesById(@NotEmpty final List ids) { + return softwareModuleRepository.findByIdIn(ids); + } + + /** + * Filter {@link SoftwareModule}s with given + * {@link SoftwareModule#getName()} or {@link SoftwareModule#getVersion()} + * and {@link SoftwareModule#getType()} that are not marked as deleted. + * + * @param pageable + * page parameter + * @param searchText + * to be filtered as "like" on {@link SoftwareModule#getName()} + * @param type + * to be filtered as "like" on {@link SoftwareModule#getType()} + * @return the page of found {@link SoftwareModule} + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Slice findSoftwareModuleByFilters(@NotNull final Pageable pageable, final String searchText, + final SoftwareModuleType type) { + + final List> specList = new ArrayList>(); + + Specification spec = SoftwareModuleSpecification.isDeletedFalse(); + specList.add(spec); + + if (!Strings.isNullOrEmpty(searchText)) { + spec = SoftwareModuleSpecification.likeNameOrVersion(searchText); + specList.add(spec); + } + + if (null != type) { + spec = SoftwareModuleSpecification.equalType(type); + specList.add(spec); + } + + spec = new Specification() { + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, + final CriteriaBuilder cb) { + if (!query.getResultType().isAssignableFrom(Long.class)) { + root.fetch(SoftwareModule_.type); + } + return cb.conjunction(); + } + }; + + specList.add(spec); + + return findSwModuleByCriteriaAPI(pageable, specList); + } + + /** + * Filter {@link SoftwareModule}s with given + * {@link SoftwareModule#getName()} or {@link SoftwareModule#getVersion()} + * and {@link SoftwareModule#getType()} that are not marked as deleted. + * + * @param pageable + * page parameter + * @param orderByDistributionId + * the ID of distribution set to be order by + * @param searchText + * to be filtered as "like" on {@link SoftwareModule#getName()} + * @param type + * to be filtered as "like" on {@link SoftwareModule#getType()} + * @return the page of found {@link SoftwareModule} + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Slice findSoftwareModuleOrderByDistribution(@NotNull final Pageable pageable, + @NotNull final Long orderByDistributionId, final String searchText, final SoftwareModuleType type) { + + final List resultList = new ArrayList<>(); + final int pageSize = pageable.getPageSize(); + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + // get the assigned software modules + final CriteriaQuery assignedQuery = cb.createQuery(SoftwareModule.class); + final Root assignedRoot = assignedQuery.from(SoftwareModule.class); + assignedQuery.distinct(true); + final ListJoin assignedDsJoin = assignedRoot.join(SoftwareModule_.assignedTo); + // build the specifications and then to predicates necessary by the + // given filters + final Predicate[] specPredicate = specificationsToPredicate(buildSpecificationList(searchText, type), + assignedRoot, assignedQuery, cb, + cb.equal(assignedDsJoin.get(DistributionSet_.id), orderByDistributionId)); + // if we have some predicates then add it to the where clause of the + // multiselect + assignedQuery.where(specPredicate); + assignedQuery.orderBy(cb.asc(assignedRoot.get(SoftwareModule_.name)), + cb.asc(assignedRoot.get(SoftwareModule_.version))); + // don't page the assigned query on database, we need all assigned + // software modules to filter + // them out in the unassigned query + final List assignedSoftwareModules = entityManager.createQuery(assignedQuery).getResultList(); + // map result + if (pageable.getOffset() < assignedSoftwareModules.size()) { + assignedSoftwareModules + .subList(pageable.getOffset(), Math.min(assignedSoftwareModules.size(), pageable.getPageSize())) + .forEach(sw -> resultList.add(new CustomSoftwareModule(sw, true))); + } + + if (assignedSoftwareModules.size() >= pageSize) { + return new SliceImpl<>(resultList); + } + + // get the unassigned software modules + final CriteriaQuery unassignedQuery = cb.createQuery(SoftwareModule.class); + unassignedQuery.distinct(true); + final Root unassignedRoot = unassignedQuery.from(SoftwareModule.class); + + Predicate[] unassignedSpec = null; + if (!assignedSoftwareModules.isEmpty()) { + unassignedSpec = specificationsToPredicate(buildSpecificationList(searchText, type), unassignedRoot, + unassignedQuery, cb, cb.not(unassignedRoot.get(SoftwareModule_.id) + .in(assignedSoftwareModules.stream().map(sw -> sw.getId()).collect(Collectors.toList())))); + } else { + unassignedSpec = specificationsToPredicate(buildSpecificationList(searchText, type), unassignedRoot, + unassignedQuery, cb); + } + + unassignedQuery.where(unassignedSpec); + unassignedQuery.orderBy(cb.asc(unassignedRoot.get(SoftwareModule_.name)), + cb.asc(unassignedRoot.get(SoftwareModule_.version))); + final List unassignedSoftwareModules = entityManager.createQuery(unassignedQuery) + .setFirstResult(Math.max(0, pageable.getOffset() - assignedSoftwareModules.size())) + .setMaxResults(pageSize).getResultList(); + // map result + unassignedSoftwareModules.forEach(sw -> resultList.add(new CustomSoftwareModule(sw, false))); + + return new SliceImpl<>(resultList); + } + + private List> buildSpecificationList(final String searchText, + final SoftwareModuleType type) { + final List> specList = new ArrayList>(); + if (!Strings.isNullOrEmpty(searchText)) { + specList.add(SoftwareModuleSpecification.likeNameOrVersion(searchText)); + } + if (type != null) { + specList.add(SoftwareModuleSpecification.equalType(type)); + } + specList.add(SoftwareModuleSpecification.isDeletedFalse()); + return specList; + } + + /** + * @param specifications + */ + private Predicate[] specificationsToPredicate(final List> specifications, + final Root root, final CriteriaQuery query, final CriteriaBuilder cb, + final Predicate... additionalPredicates) { + final List predicates = new ArrayList<>(); + specifications.forEach(spec -> predicates.add(spec.toPredicate(root, query, cb))); + for (final Predicate predicate : additionalPredicates) { + predicates.add(predicate); + } + return predicates.toArray(new Predicate[predicates.size()]); + } + + /** + * Counts {@link SoftwareModule}s with given + * {@link SoftwareModule#getName()} or {@link SoftwareModule#getVersion()} + * and {@link SoftwareModule#getType()} that are not marked as deleted. + * + * @param searchText + * to search for in name and version + * @param type + * to filter the result + * @return number of found {@link SoftwareModule}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Long countSoftwareModuleByFilters(final String searchText, final SoftwareModuleType type) { + + final List> specList = new ArrayList>(); + + Specification spec = SoftwareModuleSpecification.isDeletedFalse(); + specList.add(spec); + + if (!Strings.isNullOrEmpty(searchText)) { + spec = SoftwareModuleSpecification.likeNameOrVersion(searchText); + specList.add(spec); + } + + if (null != type) { + spec = SoftwareModuleSpecification.equalType(type); + specList.add(spec); + } + + return countSwModuleByCriteriaAPI(specList); + } + + /** + * @param pageable + * parameter + * @return all {@link SoftwareModuleType}s in the repository. + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Page findSoftwareModuleTypesAll(@NotNull final Pageable pageable) { + return softwareModuleTypeRepository.findByDeleted(pageable, false); + } + + /** + * @return number of {@link SoftwareModuleType}s in the repository. + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Long countSoftwareModuleTypesAll() { + return softwareModuleTypeRepository.countByDeleted(false); + } + + /** + * + * @param key + * to search for + * @return {@link SoftwareModuleType} in the repository with given + * {@link SoftwareModuleType#getKey()} + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public SoftwareModuleType findSoftwareModuleTypeByKey(@NotNull final String key) { + return softwareModuleTypeRepository.findByKey(key); + } + + /** + * + * @param id + * to search for + * @return {@link SoftwareModuleType} in the repository with given + * {@link SoftwareModuleType#getId()} + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public SoftwareModuleType findSoftwareModuleTypeById(@NotNull final Long id) { + return softwareModuleTypeRepository.findOne(id); + } + + /** + * + * @param name + * to search for + * @return all {@link SoftwareModuleType}s in the repository with given + * {@link SoftwareModuleType#getName()} + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public SoftwareModuleType findSoftwareModuleTypeByName(@NotNull final String name) { + return softwareModuleTypeRepository.findByName(name); + } + + /** + * Creates new {@link SoftwareModuleType}. + * + * @param type + * to create + * @return created {@link Entity} + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_REPOSITORY) + public SoftwareModuleType createSoftwareModuleType(@NotNull final SoftwareModuleType type) { + if (type.getId() != null) { + throw new EntityAlreadyExistsException("Given type contains an Id!"); + } + + return softwareModuleTypeRepository.save(type); + } + + /** + * Creates multiple {@link SoftwareModuleType}s. + * + * @param types + * to create + * @return created {@link Entity} + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_REPOSITORY) + public List createSoftwareModuleTypes(@NotNull final Collection types) { + return types.stream().map(type -> createSoftwareModuleType(type)).collect(Collectors.toList()); + } + + /** + * Deletes or markes as delete in case the type is in use. + * + * @param type + * to delete + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_DELETE_REPOSITORY) + public void deleteSoftwareModuleType(@NotNull final SoftwareModuleType type) { + + if (softwareModuleRepository.countByType(type) > 0 + || distributionSetTypeRepository.countByElementsSmType(type) > 0) { + final SoftwareModuleType toDelete = entityManager.merge(type); + toDelete.setDeleted(true); + softwareModuleTypeRepository.save(toDelete); + } else { + softwareModuleTypeRepository.delete(type.getId()); + } + } + + /** + * @param pageable + * the page request to page the result set + * @param set + * to search for + * @return all {@link SoftwareModule}s that are assigned to given + * {@link DistributionSet}. + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Page findSoftwareModuleByAssignedTo(@NotNull final Pageable pageable, + @NotNull final DistributionSet set) { + return softwareModuleRepository.findByAssignedTo(pageable, set); + } + + /** + * @param pageable + * the page request to page the result set + * @param set + * to search for + * @param type + * to filter + * @return all {@link SoftwareModule}s that are assigned to given + * {@link DistributionSet} filtered by {@link SoftwareModuleType}. + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Page findSoftwareModuleByAssignedToAndType(@NotNull final Pageable pageable, + @NotNull final DistributionSet set, @NotNull final SoftwareModuleType type) { + return softwareModuleRepository.findByAssignedToAndType(pageable, set, type); + } + + /** + * creates or updates a single software module meta data entry. + * + * @param metadata + * the meta data entry to create or update + * @return the updated or created software module meta data entry + * @throws EntityAlreadyExistsException + * in case the meta data entry already exists for the specific + * key + */ + @Transactional + @Modifying + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public SoftwareModuleMetadata createSoftwareModuleMetadata(@NotNull final SoftwareModuleMetadata metadata) { + if (softwareModuleMetadataRepository.exists(metadata.getId())) { + throwMetadataKeyAlreadyExists(metadata.getId().getKey()); + } + // merge base software module so optLockRevision gets updated and audit + // log written because + // modifying metadata is modifying the base software module itself for + // auditing purposes. + entityManager.merge(metadata.getSoftwareModule()).setLastModifiedAt(-1L); + return softwareModuleMetadataRepository.save(metadata); + } + + /** + * creates a list of software module meta data entries. + * + * @param metadata + * the meta data entries to create or update + * @return the updated or created software module meta data entries + * @throws EntityAlreadyExistsException + * in case one of the meta data entry already exists for the + * specific key + */ + @Transactional + @Modifying + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public List createSoftwareModuleMetadata( + @NotEmpty final Collection metadata) { + for (final SoftwareModuleMetadata softwareModuleMetadata : metadata) { + checkAndThrowAlreadyExistsIfSoftwareModuleMetadataExists(softwareModuleMetadata.getId()); + } + metadata.forEach(m -> entityManager.merge(m.getSoftwareModule()).setLastModifiedAt(-1L)); + return softwareModuleMetadataRepository.save(metadata); + } + + /** + * updates a distribution set meta data value if corresponding entry exists. + * + * @param metadata + * the meta data entry to be updated + * @return the updated meta data entry + * @throws EntityNotFoundException + * in case the meta data entry does not exists and cannot be + * updated + */ + @Transactional + @Modifying + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public SoftwareModuleMetadata updateSoftwareModuleMetadata(@NotNull final SoftwareModuleMetadata metadata) { + // check if exists otherwise throw entity not found exception + findOne(metadata.getId()); + // touch it to update the lock revision because we are modifying the + // software module + // indirectly + entityManager.merge(metadata.getSoftwareModule()).setLastModifiedAt(-1L); + return softwareModuleMetadataRepository.save(metadata); + } + + /** + * deletes a software module meta data entry. + * + * @param id + * the ID of the software module meta data to delete + */ + @Transactional + @Modifying + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public void deleteSoftwareModuleMetadata(@NotNull final SwMetadataCompositeKey id) { + softwareModuleMetadataRepository.delete(id); + } + + /** + * finds all meta data by the given software module id. + * + * @param swId + * the software module id to retrieve the meta data from + * @param pageable + * the page request to page the result + * @return a paged result of all meta data entries for a given software + * module id + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Page findSoftwareModuleMetadataBySoftwareModuleId(@NotNull final Long swId, + @NotNull final Pageable pageable) { + return softwareModuleMetadataRepository.findBySoftwareModuleId(swId, pageable); + } + + /** + * finds all meta data by the given software module id. + * + * @param softwareModuleId + * the software module id to retrieve the meta data from + * @param spec + * the specification to filter the result + * @param pageable + * the page request to page the result + * @return a paged result of all meta data entries for a given software + * module id + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Page findSoftwareModuleMetadataBySoftwareModuleId(final Long softwareModuleId, + @NotNull final Specification spec, @NotNull final Pageable pageable) { + return softwareModuleMetadataRepository.findAll(new Specification() { + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, + final CriteriaBuilder cb) { + + return cb.and(cb.equal(root.get(SoftwareModuleMetadata_.softwareModule).get(SoftwareModule_.id), + softwareModuleId), spec.toPredicate(root, query, cb)); + } + }, pageable); + } + + /** + * finds a single software module meta data by its id. + * + * @param id + * the id of the software module meta data containing the meta + * data key and the ID of the software module + * @return the found SoftwareModuleMetadata or {@code null} if not exits + * @throws EntityNotFoundException + * in case the meta data does not exists for the given key + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public SoftwareModuleMetadata findOne(@NotNull final SwMetadataCompositeKey id) { + final SoftwareModuleMetadata findOne = softwareModuleMetadataRepository.findOne(id); + if (findOne == null) { + throw new EntityNotFoundException("Metadata with key '" + id.getKey() + "' does not exist"); + } + return findOne; + } + + private void checkAndThrowAlreadyExistsIfSoftwareModuleMetadataExists(final SwMetadataCompositeKey metadataId) { + if (softwareModuleMetadataRepository.exists(metadataId)) { + throw new EntityAlreadyExistsException( + "Metadata entry with key '" + metadataId.getKey() + "' already exists"); + } + } + + private void throwMetadataKeyAlreadyExists(final String metadataKey) { + throw new EntityAlreadyExistsException("Metadata entry with key '" + metadataKey + "' already exists"); + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleMetadataRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleMetadataRepository.java new file mode 100755 index 000000000..0d80e1932 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleMetadataRepository.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.List; + +import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata; +import org.eclipse.hawkbit.repository.model.SwMetadataCompositeKey; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.transaction.annotation.Transactional; + +/** + * {@link SoftwareModuleMetadata} repository. + * + * + * + */ +@Transactional(readOnly = true) +public interface SoftwareModuleMetadataRepository + extends PagingAndSortingRepository, + JpaSpecificationExecutor { + + /** + * Saves all given entities. + * + * @param entities + * @return the saved entities + * @throws IllegalArgumentException + * in case the given entity is (@literal null}. + */ + @Override + List save(Iterable entities); + + /** + * finds all software module meta data of the given software module id. + * + * @param swId + * the ID of the software module to retrieve the meta data + * @param pageable + * the page request to page the result set + * @return the paged result of all meta data of an given software module id + */ + Page findBySoftwareModuleId(final Long swId, Pageable pageable); + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleRepository.java new file mode 100755 index 000000000..ccadb028a --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleRepository.java @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.List; + +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; + +/** + * {@link SoftwareModule} repository. + * + * + * + */ +@Transactional(readOnly = true) +public interface SoftwareModuleRepository + extends BaseEntityRepository, JpaSpecificationExecutor { + + /** + * Counts all {@link SoftwareModule}s based on the given {@link Type}. + * + * @param type + * to count for + * @return number of {@link SoftwareModule}s + */ + Long countByType(SoftwareModuleType type); + + /** + * Retrieves {@link SoftwareModule}s by filtering on name AND version. + * + * @param name + * to be filtered on + * @param version + * to be filtered on + * @return the found {@link SoftwareModule}s with the given name AND verion + */ + List findByNameAndVersion(String name, String version); + + /** + * deletes the {@link SoftwareModule}s with the given IDs. + * + * @param modifiedAt + * current timestamp + * @param modifiedBy + * user name of current auditor + * @param ids + * to be deleted + * + */ + @Modifying + @Transactional + @Query("UPDATE SoftwareModule b SET b.deleted = 1, b.lastModifiedAt = :lastModifiedAt, b.lastModifiedBy = :lastModifiedBy WHERE b.id IN :ids") + void deleteSoftwareModule(@Param("lastModifiedAt") Long modifiedAt, @Param("lastModifiedBy") String modifiedBy, + @Param("ids") final Long... ids); + + /** + * @param pageable + * the page request to page the result set + * @param set + * to search for + * @return all {@link SoftwareModule}s that are assigned to given + * {@link DistributionSet}. + */ + Page findByAssignedTo(Pageable pageable, DistributionSet set); + + /** + * @param set + * to search for + * @return all {@link SoftwareModule}s that are assigned to given + * {@link DistributionSet} + */ + @EntityGraph(value = "SoftwareModule.artifacts", type = EntityGraphType.LOAD) + List findByAssignedTo(DistributionSet set); + + /** + * @param pageable + * the page request to page the result set + * @param set + * to search for + * @param type + * to filter + * @return all {@link SoftwareModule}s that are assigned to given + * {@link DistributionSet} filtered by {@link SoftwareModuleType}. + */ + Page findByAssignedToAndType(Pageable pageable, DistributionSet set, SoftwareModuleType type); + + /** + * retrieves all software modules with a given {@link SoftwareModuleType} + * and {@link SoftwareModule#getId()}. + * + * @param ids + * to search for + * @param type + * to search for + * @return {@link List} of found {@link SoftwareModule}s + */ + // Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=349477 + @Query("SELECT sm FROM SoftwareModule sm WHERE sm.id IN ?1 and sm.type = ?2") + List findByIdInAndType(Iterable ids, SoftwareModuleType type); + + @Override + List save(Iterable entities); + + /** + * retrieves all software modules with a given + * {@link SoftwareModule#getId()}. + * + * @param ids + * to search for + * @return {@link List} of found {@link SoftwareModule}s + */ + // Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=349477 + @Query("SELECT sm FROM SoftwareModule sm WHERE sm.id IN ?1") + List findByIdIn(Iterable ids); +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleTypeRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleTypeRepository.java new file mode 100755 index 000000000..44b85b06b --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleTypeRepository.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.transaction.annotation.Transactional; + +/** + * Repository for {@link SoftwareModuleType}. + * + * + * + * + */ +@Transactional(readOnly = true) +public interface SoftwareModuleTypeRepository + extends BaseEntityRepository, JpaSpecificationExecutor { + + /** + * @param pageable + * @param isDeleted + * to true if only marked as deleted have to be + * count or all undeleted. + * @return found {@link SoftwareModuleType}s. + */ + Page findByDeleted(Pageable pageable, boolean isDeleted); + + /** + * @param isDeleted + * to true if only marked as deleted have to be + * count or all undeleted. + * @return number of {@link SoftwareModuleType}s in the repository. + */ + Long countByDeleted(boolean isDeleted); + + /** + * + * @param key + * to search for + * @return all {@link SoftwareModuleType}s in the repository with given + * {@link SoftwareModuleType#getKey()} + */ + SoftwareModuleType findByKey(String key); + + /** + * + * @param name + * to search for + * @return all {@link SoftwareModuleType}s in the repository with given + * {@link SoftwareModuleType#getName()} + */ + SoftwareModuleType findByName(String name); +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/SystemManagement.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/SystemManagement.java new file mode 100755 index 000000000..4ba82401d --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/SystemManagement.java @@ -0,0 +1,408 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.stream.Collectors; + +import javax.persistence.EntityManager; +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.cache.TenancyCacheManager; +import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.repository.model.TenantConfiguration; +import org.eclipse.hawkbit.repository.model.TenantMetaData; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.eclipse.hawkbit.tenancy.TenantAware.TenantRunner; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationKey; +import org.eclipse.persistence.config.PersistenceUnitProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.interceptor.SimpleKeyGenerator; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Bean; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.env.Environment; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * Central system management operations of the SP server. + * + * + * + * + */ +@Transactional(readOnly = true) +@Validated +@Service +public class SystemManagement implements EnvironmentAware { + @Autowired + private EntityManager entityManager; + + @Autowired + private TargetRepository targetRepository; + + @Autowired + private ActionRepository actionRepository; + + @Autowired + private DistributionSetRepository distributionSetRepository; + + @Autowired + private SoftwareModuleRepository softwareModuleRepository; + + @Autowired + private TenantMetaDataRepository tenantMetaDataRepository; + + @Autowired + private DistributionSetTypeRepository distributionSetTypeRepository; + + @Autowired + private SoftwareModuleTypeRepository softwareModuleTypeRepository; + + @Autowired + private TargetTagRepository targetTagRepository; + + @Autowired + private DistributionSetTagRepository distributionSetTagRepository; + + @Autowired + private ExternalArtifactRepository externalArtifactRepository; + + @Autowired + private LocalArtifactRepository artifactRepository; + + @Autowired + private ExternalArtifactProviderRepository externalArtifactProviderRepository; + + @Autowired + private TenantConfigurationRepository tenantConfigurationRepository; + + @Autowired + private TenantAware tenantAware; + + @Autowired + private TenancyCacheManager cacheManager; + + private final ThreadLocal createInitialTenant = new ThreadLocal<>(); + + private final ConfigurableConversionService conversionService = new DefaultConversionService(); + + private Environment environment; + + /** + * Registers the key generator for the {@link #currentTenant()} method + * because this key generator is aware of the {@link #createInitialTenant} + * thread local in case we are currently creating a tenant and insert the + * default distribution set types. + * + * @return the {@link CurrentTenantKeyGenerator} + */ + @Bean + public CurrentTenantKeyGenerator currentTenantKeyGenerator() { + return new CurrentTenantKeyGenerator(); + } + + /** + * Returns {@link TenantMetaData} of given and current tenant. + * + * DISCLAIMER: this variant is used during initial login (where the tenant + * is not yet in teh session). Please user {@link #getTenantMetadata()} for + * reluar requests. + * + * @param tenant + * @return + */ + @Cacheable(value = "tenantMetadata", key = "#tenant.toUpperCase()") + @Transactional + @Modifying + @NotNull + public TenantMetaData getTenantMetadata(@NotNull final String tenant) { + final TenantMetaData result = tenantMetaDataRepository.findByTenantIgnoreCase(tenant); + + // Create if it does not exist + if (result == null) { + try { + createInitialTenant.set(tenant); + cacheManager.getCache("currentTenant").evict(currentTenantKeyGenerator().generate(null, null)); + return tenantMetaDataRepository.save(new TenantMetaData(createStandardSoftwareDataSetup(), tenant)); + } finally { + createInitialTenant.remove(); + } + } + + return result; + } + + /** + * + * @return list of all tenant names in the system. + */ + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_SYSTEM_ADMIN) + // tenant independent + public List findTenants() { + return tenantMetaDataRepository.findAll().stream().map(md -> md.getTenant()).collect(Collectors.toList()); + } + + /** + * Deletes all data related to a given tenant. + * + * @param tenant + * to delete + */ + @CacheEvict(value = { "tenantMetadata" }, key = "#tenant.toUpperCase()") + @Transactional + @Modifying + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_SYSTEM_ADMIN) + // tenant independent + public void deleteTenant(@NotNull final String tenant) { + cacheManager.evictCaches(tenant); + cacheManager.getCache("currentTenant").evict(currentTenantKeyGenerator().generate(null, null)); + tenantAware.runAsTenant(tenant, new TenantRunner() { + @Override + public Void run() { + entityManager.setProperty(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT, tenant.toUpperCase()); + tenantMetaDataRepository.deleteByTenantIgnoreCase(tenant); + tenantConfigurationRepository.deleteByTenantIgnoreCase(tenant); + targetRepository.deleteByTenantIgnoreCase(tenant); + artifactRepository.deleteByTenantIgnoreCase(tenant); + externalArtifactRepository.deleteByTenantIgnoreCase(tenant); + externalArtifactProviderRepository.deleteByTenantIgnoreCase(tenant); + targetTagRepository.deleteByTenantIgnoreCase(tenant); + actionRepository.deleteByTenantIgnoreCase(tenant); + distributionSetTagRepository.deleteByTenantIgnoreCase(tenant); + distributionSetRepository.deleteByTenantIgnoreCase(tenant); + distributionSetTypeRepository.deleteByTenantIgnoreCase(tenant); + softwareModuleRepository.deleteByTenantIgnoreCase(tenant); + softwareModuleTypeRepository.deleteByTenantIgnoreCase(tenant); + return null; + } + }); + } + + /** + * @return {@link TenantMetaData} of {@link TenantAware#getCurrentTenant()} + */ + @Cacheable(value = "tenantMetadata", keyGenerator = "tenantKeyGenerator") + @Transactional + @Modifying + @NotNull + public TenantMetaData getTenantMetadata() { + if (tenantAware.getCurrentTenant() == null) { + throw new IllegalStateException("Tenant not set"); + } + + return getTenantMetadata(tenantAware.getCurrentTenant()); + } + + /** + * Checks if a specific tenant exists. The tenant will not be created lazy. + * + * @param tenant + * the tenant to check + * @return {@code true} in case the tenant exits or {@code false} if not + */ + @Cacheable(value = "currentTenant", keyGenerator = "currentTenantKeyGenerator") + // MECS-903 set transaction to not supported, due we call this in + // BaseEntity#prePersist methods + // and it seems that JPA committing the transaction when executing this + // transactional method, + // which then leads that the BaseEntity#prePersist is called again to + // persist the un-persisted + // entity and we landing again in the #currentTenant() method + // suspend the transaction here to do a read-request against the medata + // table, when the current + // tenant is not cached anyway already. + @Transactional(propagation = Propagation.NOT_SUPPORTED) + public String currentTenant() { + final String initialTenantCreation = createInitialTenant.get(); + if (initialTenantCreation == null) { + final TenantMetaData findByTenant = tenantMetaDataRepository + .findByTenantIgnoreCase(tenantAware.getCurrentTenant()); + return findByTenant != null ? findByTenant.getTenant() : null; + } + return initialTenantCreation; + } + + /** + * Update call for {@link TenantMetaData}. + * + * @param metaData + * to update + * @return updated {@link TenantMetaData} entity + */ + @CachePut(value = "tenantMetadata", key = "#metaData.tenant.toUpperCase()") + @Transactional + @Modifying + @NotNull + public TenantMetaData updateTenantMetadata(@NotNull final TenantMetaData metaData) { + if (!tenantMetaDataRepository.exists(metaData.getId())) { + throw new EntityNotFoundException("Metadata does not exist: " + metaData.getId()); + } + + return tenantMetaDataRepository.save(metaData); + } + + /** + * Retrieves a configuration value from the e.g. tenant overwritten + * configuration values or in case the tenant does not a have a specific + * configuration the global default value hold in the {@link Environment}. + * + * @param configurationKey + * the key of the configuration + * @param propertyType + * the type of the configuration value, e.g. {@code String.class} + * , {@code Integer.class}, etc + * @return the converted configuration value either from the tenant specific + * configuration stored or from the fallback default values or + * {@code null} in case key has not been configured and not default + * value exists + * @throws ConversionFailedException + * if the property cannot be converted to the given + * {@code propertyType} + */ + @Cacheable(value = "tenantConfiguration", key = "#configurationKey.getKeyName()") + public T getConfigurationValue(final TenantConfigurationKey configurationKey, final Class propertyType) { + final TenantConfiguration tenantConfiguration = tenantConfigurationRepository + .findByKey(configurationKey.getKeyName()); + if (tenantConfiguration != null) { + return conversionService.convert(tenantConfiguration.getValue(), propertyType); + } else if (configurationKey.getDefaultKeyName() != null) { + final T defaultKeyNameValue = environment.getProperty(configurationKey.getDefaultKeyName(), propertyType); + return defaultKeyNameValue != null ? defaultKeyNameValue + : conversionService.convert(configurationKey.getDefaultValue(), propertyType); + } + return null; + } + + /** + * Adds or updates a specific configuration for a specific tenant. + * + * @param tenantConf + * the tenant configuration object which contains the key and + * value of the specific configuration to update + * @return the added or updated TenantConfiguration + */ + @CacheEvict(value = "tenantConfiguration", key = "#tenantConf.getKey()") + @Transactional + @Modifying + public TenantConfiguration addOrUpdateConfiguration(final TenantConfiguration tenantConf) { + TenantConfiguration tenantConfiguration = tenantConfigurationRepository.findByKey(tenantConf.getKey()); + if (tenantConfiguration != null) { + tenantConfiguration.setValue(tenantConf.getValue()); + } else { + tenantConfiguration = new TenantConfiguration(tenantConf.getKey(), tenantConf.getValue()); + } + return tenantConfigurationRepository.save(tenantConfiguration); + } + + /** + * Deletes a specific configuration for the current tenant. + * + * @param configurationKey + * the configuration key to be deleted + */ + @CacheEvict(value = "tenantConfiguration", key = "#configurationKey.getKeyName()") + @Transactional + @Modifying + public void deleteConfiguration(final TenantConfigurationKey configurationKey) { + tenantConfigurationRepository.deleteByKey(configurationKey.getKeyName()); + } + + @Transactional + public List getTenantConfigurations() { + return tenantConfigurationRepository.findAll(); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.context.EnvironmentAware#setEnvironment(org. + * springframework.core.env. Environment) + */ + @Override + public void setEnvironment(final Environment environment) { + this.environment = environment; + } + + private DistributionSetType createStandardSoftwareDataSetup() { + + // Edge Controller Linux standard setup + final SoftwareModuleType eclApp = softwareModuleTypeRepository.save(new SoftwareModuleType("application", + "ECL Application", "Edge Controller Linux base application type", 1)); + final SoftwareModuleType eclOs = softwareModuleTypeRepository + .save(new SoftwareModuleType("os", "ECL OS", "Edge Controller Linux operation system image type", 1)); + final SoftwareModuleType eclJvm = softwareModuleTypeRepository.save( + new SoftwareModuleType("runtime", "ECL JVM", "Edge Controller Linux java virtual machine type.", 1)); + + distributionSetTypeRepository.save( + new DistributionSetType("ecl_os", "OS only", "Standard Edge Controller Linux distribution set type.") + .addMandatoryModuleType(eclOs)); + + distributionSetTypeRepository.save(new DistributionSetType("ecl_os_app", "OS with optional app", + "Standard Edge Controller Linux distribution set type. OS only.").addMandatoryModuleType(eclOs) + .addOptionalModuleType(eclApp)); + + final DistributionSetType defaultType = distributionSetTypeRepository + .save(new DistributionSetType("ecl_os_app_jvm", "OS with optional app and jvm", + "Standard Edge Controller Linux distribution set type. OS with optional application.") + .addMandatoryModuleType(eclOs).addOptionalModuleType(eclApp) + .addOptionalModuleType(eclJvm)); + + return defaultType; + } + + /** + * A implementation of the {@link KeyGenerator} to generate a key based on + * either the {@code createInitialTenant} thread local and the + * {@link TenantAware}, but in case we are in a tenant creation with its + * default types we need to use the tenant the current tenant which is + * currently created and not the one currently in the {@link TenantAware}. + * + * + * + */ + private class CurrentTenantKeyGenerator implements KeyGenerator { + + /* + * (non-Javadoc) + * + * @see + * org.springframework.cache.interceptor.KeyGenerator#generate(java.lang + * .Object, java.lang.reflect.Method, java.lang.Object[]) + */ + @Override + public Object generate(final Object target, final Method method, final Object... params) { + final String initialTenantCreation = createInitialTenant.get(); + if (initialTenantCreation == null) { + return SimpleKeyGenerator.generateKey(tenantAware.getCurrentTenant().toUpperCase(), + tenantAware.getCurrentTenant().toUpperCase()); + } + return SimpleKeyGenerator.generateKey(initialTenantCreation.toUpperCase(), + initialTenantCreation.toUpperCase()); + } + + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TagManagement.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TagManagement.java new file mode 100755 index 000000000..541c9c9cf --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TagManagement.java @@ -0,0 +1,343 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.LinkedList; +import java.util.List; + +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetTag; +import org.eclipse.hawkbit.repository.model.Tag; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetTag; +import org.hibernate.validator.constraints.NotEmpty; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * + * Mangement service class for {@link Tag}s. + * + * + * + * + * + */ +@Transactional(readOnly = true) +@Validated +@Service +public class TagManagement { + + @Autowired + private TargetTagRepository targetTagRepository; + + @Autowired + private TargetRepository targetRepository; + + @Autowired + private DistributionSetTagRepository distributionSetTagRepository; + + @Autowired + private DistributionSetRepository distributionSetRepository; + + /** + * Find {@link TargetTag} based on given Name. + * + * @param name + * to look for. + * @return {@link TargetTag} or null if it does not exist + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public TargetTag findTargetTag(@NotEmpty final String name) { + return targetTagRepository.findByNameEquals(name); + } + + /** + * Creates a new {@link TargetTag}. + * + * @param targetTag + * to be created + * + * @return the new created {@link TargetTag} + * + * @throws EntityAlreadyExistsException + * if given object already exists + */ + @Modifying + @Transactional + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_TARGET) + public TargetTag createTargetTag(@NotNull final TargetTag targetTag) { + if (null != targetTag.getId()) { + throw new EntityAlreadyExistsException(); + } + + if (findTargetTag(targetTag.getName()) != null) { + throw new EntityAlreadyExistsException(); + } + + return targetTagRepository.save(targetTag); + } + + /** + * created multiple {@link TargetTag}s. + * + * @param targetTags + * to be created + * @return the new created {@link TargetTag}s + * + * @throws EntityAlreadyExistsException + * if given object has already an ID. + */ + @Modifying + @Transactional + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_TARGET) + public List createTargetTags(@NotNull final Iterable targetTags) { + targetTags.forEach(tag -> { + if (tag.getId() != null) { + throw new EntityAlreadyExistsException(); + } + }); + + return targetTagRepository.save(targetTags); + } + + /** + * Deletes {@link TargetTag} with given name. + * + * @param targetTagName + * tag name of the {@link TargetTag} to be deleted + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_DELETE_TARGET) + public void deleteTargetTag(@NotEmpty final String targetTagName) { + final TargetTag tag = targetTagRepository.findByNameEquals(targetTagName); + + final List changed = new LinkedList(); + for (final Target target : targetRepository.findByTag(tag)) { + target.getTags().remove(tag); + changed.add(target); + } + + // save association delete + targetRepository.save(changed); + + // finally delete the tag itself + targetTagRepository.deleteByName(targetTagName); + } + + /** + * returns all {@link TargetTag}s. + * + * @return all {@link TargetTag}s + */ + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public List findAllTargetTags() { + return targetTagRepository.findAll(); + } + + /** + * updates the {@link TargetTag}. + * + * @param targetTag + * the {@link TargetTag} + * @return the new {@link TargetTag} + */ + @Modifying + @Transactional + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) + public TargetTag updateTargetTag(@NotNull final TargetTag targetTag) { + checkNotNull(targetTag.getName()); + checkNotNull(targetTag.getId()); + return targetTagRepository.save(targetTag); + } + + /** + * Find {@link DistributionSet} based on given name. + * + * @param name + * to look for. + * @return {@link DistributionSet} or null if it does not exist + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public DistributionSetTag findDistributionSetTag(@NotEmpty final String name) { + return distributionSetTagRepository.findByNameEquals(name); + } + + /** + * Creates a {@link DistributionSet}. + * + * @param distributionSetTag + * to be created. + * @return the new {@link DistributionSet} + * @throws EntityAlreadyExistsException + * if distributionSetTag already exists + * + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_REPOSITORY) + public DistributionSetTag createDistributionSetTag(@NotNull final DistributionSetTag distributionSetTag) { + if (null != distributionSetTag.getId()) { + throw new EntityAlreadyExistsException(); + } + + if (distributionSetTagRepository.findByNameEquals(distributionSetTag.getName()) != null) { + throw new EntityAlreadyExistsException(); + } + + return distributionSetTagRepository.save(distributionSetTag); + } + + /** + * Creates multiple {@link DistributionSetTag}s. + * + * @param distributionSetTags + * to be created + * @return the new {@link DistributionSetTag} + * @throws EntityAlreadyExistsException + * if a given entity already exists + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_REPOSITORY) + public Iterable createDistributionSetTags( + @NotNull final Iterable distributionSetTags) { + for (final DistributionSetTag dsTag : distributionSetTags) { + if (dsTag.getId() != null) { + throw new EntityAlreadyExistsException(); + } + } + + return distributionSetTagRepository.save(distributionSetTags); + } + + /** + * Deletes {@link DistributionSetTag} by given + * {@link DistributionSetTag#getName()}. + * + * @param tagNames + * to be deleted + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_DELETE_REPOSITORY) + public void deleteDistributionSetTag(@NotEmpty final String tagName) { + final DistributionSetTag tag = distributionSetTagRepository.findByNameEquals(tagName); + + final List changed = new LinkedList(); + for (final DistributionSet set : distributionSetRepository.findByTag(tag)) { + set.getTags().remove(tag); + changed.add(set); + } + + // save association delete + distributionSetRepository.save(changed); + + distributionSetTagRepository.deleteByName(tagName); + } + + /** + * Updates an existing {@link DistributionSetTag}. + * + * @param distributionSetTag + * to be updated + * @return the updated {@link DistributionSet} + * @throws NullPointerException + * of {@link DistributionSetTag#getName()} is null + */ + @Modifying + @Transactional + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) + public DistributionSetTag updateDistributionSetTag(@NotNull final DistributionSetTag distributionSetTag) { + checkNotNull(distributionSetTag.getName()); + checkNotNull(distributionSetTag.getId()); + return distributionSetTagRepository.save(distributionSetTag); + } + + /** + * returns all {@link DistributionTag}s. + * + * @return all {@link DistributionTag}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public List findDistributionSetTagsAll() { + return distributionSetTagRepository.findAll(); + } + + /** + * Finds {@link TargetTag} by given id. + * + * @param id + * to search for + * @return the found {@link TargetTag}s or null if not found. + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public TargetTag findTargetTagById(@NotNull final Long id) { + return targetTagRepository.findOne(id); + } + + /** + * Finds {@link DistributionSetTag} by given id. + * + * @param id + * to search for + * @return the found {@link DistributionSetTag}s or null if not + * found. + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public DistributionSetTag findDistributionSetTagById(@NotNull final Long id) { + return distributionSetTagRepository.findOne(id); + } + + /** + * returns all {@link TargetTag}s. + * + * @param pageReq + * page parameter + * + * @return all {@link TargetTag}s + */ + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Page findAllTargetTags(@NotNull final Pageable pageReq) { + return targetTagRepository.findAll(pageReq); + } + + /** + * returns all {@link TargetTag}s. + * + * @param pageReq + * page parameter + * @return all {@link TargetTag}s + */ + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) + public Page findDistributionSetTagsAll(@NotNull final Pageable pageReq) { + return distributionSetTagRepository.findAll(pageReq); + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java new file mode 100755 index 000000000..11781559e --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java @@ -0,0 +1,180 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.ArrayList; +import java.util.List; + +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.model.TargetFilterQuery; +import org.eclipse.hawkbit.repository.specifications.TargetFilterQuerySpecification; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.domain.Specifications; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.springframework.validation.annotation.Validated; + +import com.google.common.base.Strings; + +/** + * Business service facade for managing {@link TargetFilterQuery}s. + * + * + * + */ +@Transactional(readOnly = true) +@Validated +@Service +public class TargetFilterQueryManagement { + + @Autowired + private TargetFilterQueryRepository targetFilterQueryRepository; + + /** + * creating new {@link TargetFilterQuery}. + * + * @param customTargetFilter + * @return the created {@link TargetFilterQuery} + */ + @Modifying + @Transactional + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_TARGET) + public TargetFilterQuery createTargetFilterQuery(@NotNull final TargetFilterQuery customTargetFilter) { + + if (targetFilterQueryRepository.findByName(customTargetFilter.getName()) != null) { + throw new EntityAlreadyExistsException(customTargetFilter.getName()); + } + final TargetFilterQuery filter = targetFilterQueryRepository.save(customTargetFilter); + return filter; + } + + /** + * Delete target filter query. + * + * @param targetFilterQueryId + * IDs of target filter query to be deleted + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_DELETE_TARGET) + public void deleteTargetFilterQuery(@NotNull final Long targetFilterQueryId) { + targetFilterQueryRepository.delete(targetFilterQueryId); + } + + /** + * + * Retrieves all target filter query{@link TargetFilterQuery}. + * + * @param pageable + * pagination parameter + * @return the found {@link TargetFilterQuery}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Page findAllTargetFilterQuery(@NotNull final Pageable pageable) { + return targetFilterQueryRepository.findAll(pageable); + } + + /** + * Retrieves all target filter query which {@link TargetFilterQuery}. + * + * + * @param pageable + * pagination parameter + * @param name + * target filter query name + * @return the page with the found {@link TargetFilterQuery} + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Page findTargetFilterQueryByFilters(@NotNull final Pageable pageable, final String name) { + final List> specList = new ArrayList>(); + if (!Strings.isNullOrEmpty(name)) { + specList.add(TargetFilterQuerySpecification.likeName(name)); + } + return findTargetFilterQueryByCriteriaAPI(pageable, specList); + } + + /** + * + * @param pageable + * pagination parameter + * @param specList + * list of @link {@link Specification} + * @return the page with the found {@link TargetFilterQuery} + */ + private Page findTargetFilterQueryByCriteriaAPI(@NotNull final Pageable pageable, + final List> specList) { + Specifications specs = null; + if (!specList.isEmpty()) { + specs = Specifications.where(specList.get(0)); + } + if (specList.size() > 1) { + for (final Specification s : specList.subList(1, specList.size())) { + specs = specs.and(s); + } + } + if (specs == null) { + return targetFilterQueryRepository.findAll(pageable); + } else { + return targetFilterQueryRepository.findAll(specs, pageable); + } + } + + /** + * Find target filter query by name. + * + * @param targetFilterQueryName + * Target filter query name + * @return the found {@link TargetFilterQuery} + * + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public TargetFilterQuery findTargetFilterQueryByName(@NotNull final String targetFilterQueryName) { + return targetFilterQueryRepository.findByName(targetFilterQueryName); + } + + /** + * Find target filter query by id. + * + * @param targetFilterQueryId + * Target filter query id + * @return the found {@link TargetFilterQuery} + * + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public TargetFilterQuery findTargetFilterQueryById(@NotNull final Long targetFilterQueryId) { + return targetFilterQueryRepository.findOne(targetFilterQueryId); + } + + /** + * updates the {@link TargetFilterQuery}. + * + * @param targetFilterQuery + * to be updated + * @return the updated {@link TargetFilterQuery} + */ + @Modifying + @Transactional + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) + public TargetFilterQuery updateTargetFilterQuery(@NotNull final TargetFilterQuery targetFilterQuery) { + Assert.notNull(targetFilterQuery.getId()); + return targetFilterQueryRepository.save(targetFilterQuery); + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryRepository.java new file mode 100755 index 000000000..e78b02286 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryRepository.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import org.eclipse.hawkbit.repository.model.TargetFilterQuery; +import org.springframework.data.domain.Page; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.transaction.annotation.Transactional; + +/** + * + * + */ +@Transactional(readOnly = true) +public interface TargetFilterQueryRepository extends + BaseEntityRepository, + JpaSpecificationExecutor { + + /** + * Find customer target filter by name + * + * @param name + * @return custom target filter + */ + TargetFilterQuery findByName(final String name); + + /** + * Find list of all custom target filters. + */ + @Override + Page findAll(); + + @Override + @Modifying + @Transactional + S save(S entity); + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetInfoRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetInfoRepository.java new file mode 100755 index 000000000..4582b7397 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetInfoRepository.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.Collection; +import java.util.List; + +import javax.persistence.Entity; +import javax.transaction.Transactional; + +import org.eclipse.hawkbit.repository.model.TargetInfo; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +/** + * Usually a JPA spring data repository to handle {@link TargetInfo} entity. + * However, do to an eclipselink bug with spring boot now a regular interface + * that is implemented by {@link EclipseLinkTargetInfoRepository}. + * + * + * + */ +public interface TargetInfoRepository { + + /** + * Sets new TargetUpdateStatus of given target if is not already on that + * value. + * + * @param status + * to set + * @param targets + * to set it for + */ + @Modifying + @Transactional + @Query("update TargetInfo ti set ti.updateStatus = :status where ti.targetId in :targets and ti.updateStatus != :status") + void setTargetUpdateStatus(@Param("status") TargetUpdateStatus status, @Param("targets") List targets); + + /** + * Save entity and evict cache with it. + * + * @param entity + * to persists + * + * @return persisted or updated {@link Entity} + */ + @CacheEvict(value = { "targetStatus", "distributionUsageInstalled", "targetsLastPoll" }, allEntries = true) + S save(S entity); + + /** + * Deletes info entries by ID. + * + * @param targetIDs + * to delete + */ + @Modifying + @Transactional + @CacheEvict(value = { "targetStatus", "distributionUsageInstalled", "targetsLastPoll" }, allEntries = true) + void deleteByTargetIdIn(final Collection targetIDs); +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java new file mode 100755 index 000000000..c0a635442 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java @@ -0,0 +1,1003 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import javax.annotation.PreDestroy; +import javax.persistence.Entity; +import javax.persistence.EntityManager; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Expression; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.JoinType; +import javax.persistence.criteria.Order; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.Constants; +import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSet_; +import org.eclipse.hawkbit.repository.model.Tag; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetFilterQuery; +import org.eclipse.hawkbit.repository.model.TargetIdName; +import org.eclipse.hawkbit.repository.model.TargetInfo; +import org.eclipse.hawkbit.repository.model.TargetInfo_; +import org.eclipse.hawkbit.repository.model.TargetTag; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.eclipse.hawkbit.repository.model.Target_; +import org.eclipse.hawkbit.repository.rsql.RSQLUtility; +import org.eclipse.hawkbit.repository.specifications.TargetSpecifications; +import org.hibernate.validator.constraints.NotEmpty; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.domain.Specifications; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.springframework.validation.annotation.Validated; + +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import com.google.common.eventbus.EventBus; + +/** + * Business service facade for managing {@link Target}s. + * + * + * + */ +@Transactional(readOnly = true) +@Validated +@Service +public class TargetManagement { + + @Autowired + private EntityManager entityManager; + + @Autowired + private TargetRepository targetRepository; + + @Autowired + private TargetTagRepository targetTagRepository; + + @Autowired + private TargetFilterQueryRepository targetFilterQueryRepository; + + @Autowired + private TargetInfoRepository targetInfoRepository; + + @Autowired + private NoCountPagingRepository criteriaNoCountDao; + + @Autowired + private EventBus eventBus; + + /** + * Find {@link Target} based on given ID returns found Target without + * details, i.e. NO {@link Target#getTags()} and + * {@link Target#getActiveActions()} possible. + * + * @param controllerId + * to look for. + * @return {@link Target} or null if it does not exist + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Target findTargetByControllerID(@NotEmpty final String controllerId) { + return targetRepository.findByControllerId(controllerId); + } + + /** + * Find {@link Target} based on given ID returns found Target with details, + * i.e. {@link Target#getTags()} and {@link Target#getActiveActions()} are + * possible. + * + * Note: try to use {@link #findTargetByControllerID(String)} as much as + * possible. + * + * @param controllerId + * to look for. + * @return {@link Target} or null if it does not exist + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Target findTargetByControllerIDWithDetails(@NotEmpty final String controllerId) { + final Target result = targetRepository.findByControllerId(controllerId); + // load lazy relations + if (result != null) { + result.getTargetInfo().getControllerAttributes().size(); + if (result.getTargetInfo() != null && result.getTargetInfo().getInstalledDistributionSet() != null) { + result.getTargetInfo().getInstalledDistributionSet().getName(); + result.getTargetInfo().getInstalledDistributionSet().getModules().size(); + } + if (result.getAssignedDistributionSet() != null) { + result.getAssignedDistributionSet().getName(); + result.getAssignedDistributionSet().getModules().size(); + } + } + return result; + } + + /** + * Find {@link Target} based on given ID returns found Target without + * details, i.e. NO {@link Target#getTags()} and + * {@link Target#getActiveActions()} possible. + * + * @param controllerIDs + * to look for. + * @return List of found{@link Target}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public List findTargetsByControllerID(@NotEmpty final Collection controllerIDs) { + return targetRepository.findAll(TargetSpecifications.byControllerIdWithStatusAndAssignedInJoin(controllerIDs)); + } + + /** + * Counts all {@link Target}s in the repository. + * + * @return number of targets + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Long countTargetsAll() { + return targetRepository.count(); + } + + /** + * Retrieves all targets without details, i.e. NO {@link Target#getTags()} + * and {@link Target#getActiveActions()} possible + * + * @param pageable + * pagination parameter + * @return the found {@link Target}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Slice findTargetsAll(@NotNull final Pageable pageable) { + // workarround - no join fetch allowed that is why we need specification + // instead of query for + // count() of Pageable + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { + if (!query.getResultType().isAssignableFrom(Long.class)) { + root.fetch(Target_.targetInfo); + } + return cb.conjunction(); + } + }; + return criteriaNoCountDao.findAll(spec, pageable, Target.class); + } + + /** + * Retrieves all targets without details, i.e. NO {@link Target#getTags()} + * and {@link Target#getActiveActions()} possible based on + * {@link TargetFilterQuery#getQuery()} + * + * @param targetFilterQuery + * @param pageable + * @return + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Slice findTargetsAll(@NotNull final TargetFilterQuery targetFilterQuery, + @NotNull final Pageable pageable) { + return findTargetsAll(RSQLUtility.parse(targetFilterQuery.getQuery(), TargetFields.class, entityManager), + pageable); + } + + /** + * Retrieves all targets without details, i.e. NO {@link Target#getTags()} + * and {@link Target#getActiveActions()} possible based on + * {@link TargetFilterQuery#getQuery()} + * + * @param targetFilterQuery + * @param pageable + * @return + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Slice findTargetsAll(@NotNull final String targetFilterQuery, @NotNull final Pageable pageable) { + return findTargetsAll(RSQLUtility.parse(targetFilterQuery, TargetFields.class, entityManager), pageable); + } + + /** + * Retrieves all targets based on the given specification. + * + * @param spec + * the specification for the query + * @param pageable + * pagination parameter + * @return the found {@link Target}s, never {@code null} + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Page findTargetsAll(@NotNull final Specification spec, @NotNull final Pageable pageable) { + return targetRepository.findAll(spec, pageable); + } + + /** + * retrieves a list of {@link Target}s by their controller ID with details, + * i.e. {@link Target#getTags()} are possible. + * + * Note: try to use {@link #findTargetByControllerID(String)} as much as + * possible. + * + * @param controllerIDs + * {@link Target}s Names parameter + * @return the found {@link Target}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public List findTargetsByControllerIDsWithTags(@NotNull final List controllerIDs) { + final List> partition = Lists.partition(controllerIDs, Constants.MAX_ENTRIES_IN_STATEMENT); + return partition.stream() + .map(ids -> targetRepository.findAll(TargetSpecifications.byControllerIdWithStatusAndTagsInJoin(ids))) + .flatMap(t -> t.stream()).collect(Collectors.toList()); + } + + /** + * updates the {@link Target}. + * + * @param target + * to be updated + * @return the updated {@link Target} + */ + @Modifying + @Transactional + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET + SpringEvalExpressions.HAS_AUTH_OR + + SpringEvalExpressions.IS_CONTROLLER) + public Target updateTarget(@NotNull final Target target) { + Assert.notNull(target.getId()); + target.setNew(false); + return targetRepository.save(target); + } + + /** + * updates multiple {@link Target}s. + * + * @param targets + * to be updated + * @return the updated {@link Target}s + */ + @Modifying + @Transactional + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET + SpringEvalExpressions.HAS_AUTH_OR + + SpringEvalExpressions.IS_CONTROLLER) + public List updateTargets(@NotNull final List targets) { + targets.forEach(target -> target.setNew(false)); + return targetRepository.save(targets); + } + + /** + * Deletes all targets with the given IDs. + * + * @param targetIDs + * the technical IDs of the targets to be deleted + */ + @Modifying + @Transactional + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_DELETE_TARGET) + public void deleteTargets(@NotEmpty final Long... targetIDs) { + // we need to select the target IDs first to check the if the targetIDs + // belonging to the + // tenant! Delete statement are not automatically enhanced with the + // @FilterDef of the + // hibernate session. + final List targetsForCurrentTenant = targetRepository.findAll(Lists.newArrayList(targetIDs)).stream() + .map(Target::getId).collect(Collectors.toList()); + if (!targetsForCurrentTenant.isEmpty()) { + targetInfoRepository.deleteByTargetIdIn(targetsForCurrentTenant); + targetRepository.deleteByIdIn(targetsForCurrentTenant); + } + } + + /** + * retrieves {@link Target}s by the assigned {@link DistributionSet} without + * details, i.e. NO {@link Target#getTags()} and + * {@link Target#getActiveActions()} possible. + * + * + * @param distributionSetID + * the ID of the {@link DistributionSet} + * @param pageReq + * page parameter + * @return the found {@link Target}s + */ + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_READ_TARGET) + public Page findTargetByAssignedDistributionSet(@NotNull final Long distributionSetID, + @NotNull final Pageable pageReq) { + return targetRepository.findByAssignedDistributionSetId(pageReq, distributionSetID); + } + + /** + * retrieves {@link Target}s by the assigned {@link DistributionSet} without + * details, i.e. NO {@link Target#getTags()} and + * {@link Target#getActiveActions()} possible including the filtering based + * on the given {@code spec}. + * + * @param distributionSetID + * the ID of the {@link DistributionSet} + * @param spec + * the specification to filter the result set + * @param pageReq + * page parameter + * @return the found {@link Target}s, never {@code null} + */ + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_READ_TARGET) + public Page findTargetByAssignedDistributionSet(@NotNull final Long distributionSetID, + final Specification spec, @NotNull final Pageable pageReq) { + return targetRepository.findAll(new Specification() { + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { + return cb + .and(TargetSpecifications.hasAssignedDistributionSet(distributionSetID).toPredicate(root, + query, cb), spec.toPredicate(root, query, cb)); + } + }, pageReq); + } + + /** + * retrieves {@link Target}s by the installed {@link DistributionSet}without + * details, i.e. NO {@link Target#getTags()} and + * {@link Target#getActiveActions()} possible. + * + * @param distributionSetID + * the ID of the {@link DistributionSet} + * @param pageReq + * page parameter + * @return the found {@link Target}s + */ + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_READ_TARGET) + public Page findTargetByInstalledDistributionSet(@NotNull final Long distributionSetID, + @NotNull final Pageable pageReq) { + return targetRepository.findByTargetInfoInstalledDistributionSetId(pageReq, distributionSetID); + } + + /** + * retrieves {@link Target}s by the installed {@link DistributionSet}without + * details, i.e. NO {@link Target#getTags()} and + * {@link Target#getActiveActions()} possible. + * + * @param distributionSetId + * the ID of the {@link DistributionSet} + * @param spec + * the specification to filter the result + * @param pageable + * page parameter + * @return the found {@link Target}s, never {@code null} + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_READ_TARGET) + public Page findTargetByInstalledDistributionSet(final Long distributionSetId, + final Specification spec, final Pageable pageable) { + return targetRepository.findAll(new Specification() { + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { + return cb.and( + TargetSpecifications.hasInstalledDistributionSet(distributionSetId) + .toPredicate(root, query, cb), spec.toPredicate(root, query, cb)); + } + }, pageable); + } + + /** + * Retrieves the {@link Target} which have a certain + * {@link TargetUpdateStatus} without details, i.e. NO + * {@link Target#getTags()} and {@link Target#getActiveActions()} possible. + * + * @param pageable + * page parameter + * @param status + * the {@link TargetUpdateStatus} to be filtered on + * @return the found {@link Target}s + */ + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Page findTargetByUpdateStatus(@NotNull final Pageable pageable, + @NotNull final TargetUpdateStatus status) { + return targetRepository.findByTargetInfoUpdateStatus(pageable, status); + } + + /** + * Filter {@link Target}s for all the given parameters. If all parameters + * except pageable are null, all available {@link Target}s are returned. + * + * @param pageable + * page parameters + * @param status + * find targets having this {@link TargetUpdateStatus}s. Set to + * null in case this is not required. + * @param searchText + * to find targets having the text anywhere in name or + * description. Set null in case this is not + * required. + * @param installedOrAssignedDistributionSetId + * to find targets having the {@link DistributionSet} as + * installed or assigned. Set to null in case this + * is not required. + * @param tagNames + * to find targets which are having any one in this tag names. + * Set null in case this is not required. + * @param selectTargetWithNoTag + * flag to select targets with no tag assigned + * + * @return the found {@link Target}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Slice findTargetByFilters(@NotNull final Pageable pageable, + final Collection status, final String searchText, + final Long installedOrAssignedDistributionSetId, final Boolean selectTargetWithNoTag, + final String... tagNames) { + final List> specList = buildSpecificationList(status, searchText, + installedOrAssignedDistributionSetId, selectTargetWithNoTag, true, tagNames); + return findByCriteriaAPI(pageable, specList); + } + + /** + * Count {@link Target}s for all the given filter parameters. + * + * @param status + * find targets having on of these {@link TargetUpdateStatus}s. + * Set to null in case this is not required. + * @param searchText + * to find targets having the text anywhere in name or + * description. Set null in case this is not + * required. + * @param installedOrAssignedDistributionSetId + * to find targets having the {@link DistributionSet} as + * installed or assigned. Set to null in case this + * is not required. + * @param tagNames + * to find targets which are having any one in this tag names. + * Set null in case this is not required. + * @param selectTargetWithNoTag + * flag to select targets with no tag assigned + * + * @return the found number {@link Target}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Long countTargetByFilters(final Collection status, final String searchText, + final Long installedOrAssignedDistributionSetId, final Boolean selectTargetWithNoTag, + final String... tagNames) { + final List> specList = buildSpecificationList(status, searchText, + installedOrAssignedDistributionSetId, selectTargetWithNoTag, true, tagNames); + return countByCriteriaAPI(specList); + } + + private List> buildSpecificationList(final Collection status, + final String searchText, final Long installedOrAssignedDistributionSetId, + final Boolean selectTargetWithNoTag, final boolean fetch, final String... tagNames) { + final List> specList = new ArrayList>(); + if (status != null && !status.isEmpty()) { + specList.add(TargetSpecifications.hasTargetUpdateStatus(status, fetch)); + } + if (installedOrAssignedDistributionSetId != null) { + specList.add(TargetSpecifications + .hasInstalledOrAssignedDistributionSet(installedOrAssignedDistributionSetId)); + } + if (!Strings.isNullOrEmpty(searchText)) { + specList.add(TargetSpecifications.likeNameOrDescriptionOrIp(searchText)); + } + if (selectTargetWithNoTag || (tagNames != null && tagNames.length > 0)) { + specList.add(TargetSpecifications.hasTags(tagNames, selectTargetWithNoTag)); + } + return specList; + } + + /** + * executes findAll with the given {@link Target} {@link Specification}s. + * + * @param pageable + * paging parameter + * @param specList + * list of @link {@link Specification} + * @return the page with the found {@link Target} + */ + private Slice findByCriteriaAPI(final Pageable pageable, final List> specList) { + final Specifications specs = creatingTargetSpecifications(specList); + + if (specs == null) { + return criteriaNoCountDao.findAll(pageable, Target.class); + } else { + return criteriaNoCountDao.findAll(specs, pageable, Target.class); + } + + } + + private Long countByCriteriaAPI(final List> specList) { + final Specifications specs = creatingTargetSpecifications(specList); + + if (specs == null) { + return targetRepository.count(); + } else { + return targetRepository.count(specs); + } + + } + + private Specifications creatingTargetSpecifications(final List> specList) { + Specifications specs = null; + if (!specList.isEmpty()) { + specs = Specifications.where(specList.get(0)); + } + if (specList.size() > 1) { + specList.remove(0); + for (final Specification s : specList) { + specs = specs.and(s); + } + } + return specs; + } + + /** + * {@link Entity} based method call for + * {@link #toggleTagAssignment(Collection, String)}. + * + * @param targets + * to toggle for + * @param tag + * to toogle + * @return TagAssigmentResult with all metadata of the assigment outcome. + */ + @Modifying + @Transactional + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) + public TargetTagAssigmentResult toggleTagAssignment(@NotEmpty final List targets, + @NotNull final TargetTag tag) { + return toggleTagAssignment(targets.stream().map(target -> target.getControllerId()) + .collect(Collectors.toList()), tag.getName()); + } + + /** + * Toggles {@link TargetTag} assignment to given {@link Target}s by means + * that if some (or all) of the targets in the list have the {@link Tag} not + * yet assigned, they will be. If all of theme have the tag already assigned + * they will be removed instead. + * + * @param targetIds + * to toggle for + * @param tagName + * to toogle + * @return TagAssigmentResult with all metadata of the assigment outcome. + */ + @Modifying + @Transactional + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) + public TargetTagAssigmentResult toggleTagAssignment(@NotEmpty final Collection targetIds, + @NotNull final String tagName) { + final TargetTag tag = targetTagRepository.findByNameEquals(tagName); + final List alreadyAssignedTargets = targetRepository.findByTagNameAndControllerIdIn(tagName, targetIds); + final List allTargets = targetRepository.findAll(TargetSpecifications + .byControllerIdWithStatusAndTagsInJoin(targetIds)); + + // all are already assigned -> unassign + if (alreadyAssignedTargets.size() == allTargets.size()) { + alreadyAssignedTargets.forEach(target -> target.getTags().remove(tag)); + return new TargetTagAssigmentResult(0, 0, alreadyAssignedTargets.size(), null, alreadyAssignedTargets); + } + + allTargets.removeAll(alreadyAssignedTargets); + // some or none are assigned -> assign + allTargets.forEach(target -> target.getTags().add(tag)); + final TargetTagAssigmentResult result = new TargetTagAssigmentResult(alreadyAssignedTargets.size(), + allTargets.size(), 0, targetRepository.save(allTargets), null); + // no reason to persist the tag + entityManager.detach(tag); + return result; + } + + /** + * method retrieves all {@link Target}s from the repo in the following + * order: + *

+ * 1) {@link Target}s which have the given {@link DistributionSet} as + * {@link Target#getTargetStatus()} + * {@link TargetStatus#getInstalledDistributionSet()} + *

+ * 2) {@link Target}s which have the given {@link DistributionSet} as + * {@link Target#getAssignedDistributionSet()} + *

+ * 3) {@link Target}s which have no connection to the given + * {@link DistributionSet}. + * + * @param pageable + * the page request to page the result set + * @param orderByDistributionId + * {@link DistributionSet#getId()} to be ordered by + * @param filterByDistributionId + * {@link DistributionSet#getId()} to be filter the result. Set + * to null in case this is not required. + * @param filterByStatus + * find targets having this {@link TargetUpdateStatus}s. Set to + * null in case this is not required. + * @param filterBySearchText + * to find targets having the text anywhere in name or + * description. Set null in case this is not + * required. + * @param installedOrAssignedDistributionSetId + * to find targets having the {@link DistributionSet} as + * installed or assigned. Set to null in case this + * is not required. + * @param filterByTagNames + * to find targets which are having any one in this tag names. + * Set null in case this is not required. + * @param selectTargetWithNoTag + * flag to select targets with no tag assigned + * @return a paged result {@link Page} of the {@link Target}s in a defined + * order. + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Slice findTargetsAllOrderByLinkedDistributionSet(@NotNull final Pageable pageable, + @NotNull final Long orderByDistributionId, final Long filterByDistributionId, + final Collection filterByStatus, final String filterBySearchText, + final Boolean selectTargetWithNoTag, final String... filterByTagNames) { + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + final CriteriaQuery query = cb.createQuery(Target.class); + final Root targetRoot = query.from(Target.class); + + // necessary joins for the select + final Join targetInfo = (Join) targetRoot.fetch(Target_.targetInfo, + JoinType.LEFT); + + // select case expression to retrieve the case value as a column to be + // able to order based on + // this column, installed first,... + final Expression selectCase = cb + .selectCase() + .when(cb.equal(targetInfo.get(TargetInfo_.installedDistributionSet).get(DistributionSet_.id), + orderByDistributionId), 1) + .when(cb.equal(targetRoot.get(Target_.assignedDistributionSet).get(DistributionSet_.id), + orderByDistributionId), 2).otherwise(100); + // multiselect statement order by the select case and controllerId + query.distinct(true); + // build the specifications and then to predicates necessary by the + // given filters + final Predicate[] specificationsForMultiSelect = specificationsToPredicate( + buildSpecificationList(filterByStatus, filterBySearchText, filterByDistributionId, + selectTargetWithNoTag, true, filterByTagNames), targetRoot, query, cb); + + // if we have some predicates then add it to the where clause of the + // multiselect + if (specificationsForMultiSelect.length > 0) { + query.where(specificationsForMultiSelect); + } + // add the order to the multi select first based on the selectCase + query.orderBy(cb.asc(selectCase), cb.desc(targetRoot.get(Target_.id))); + // the result is a Object[] due the fact that the selectCase is an extra + // column, so it cannot + // be mapped directly to a Target entity because the selectCase is not a + // attribute of the + // Target entity, the the Object array contains the Target on the first + // index (case of the + // multiselect order) of the array and + // the 2nd contains the selectCase int value. + final int pageSize = pageable.getPageSize(); + final List resultList = entityManager.createQuery(query).setFirstResult(pageable.getOffset()) + .setMaxResults(pageSize + 1).getResultList(); + final boolean hasNext = resultList.size() > pageSize; + return new SliceImpl<>(resultList, pageable, hasNext); + } + + /** + * @param specifications + */ + private Predicate[] specificationsToPredicate(final List> specifications, + final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { + final Predicate[] predicates = new Predicate[specifications.size()]; + for (int index = 0; index < predicates.length; index++) { + predicates[index] = specifications.get(index).toPredicate(root, query, cb); + } + return predicates; + } + + /** + * Counts number of targets with given + * {@link Target#getAssignedDistributionSet()}. + * + * @param distId + * to search for + * + * @return number of found {@link Target}s. + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Long countTargetByAssignedDistributionSet(final Long distId) { + return targetRepository.countByAssignedDistributionSetId(distId); + } + + /** + * Counts number of targets with given + * {@link TargetStatus#getInstalledDistributionSet()}. + * + * @param distId + * to search for + * @return number of found {@link Target}s. + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Long countTargetByInstalledDistributionSet(final Long distId) { + return targetRepository.countByTargetInfoInstalledDistributionSetId(distId); + } + + /** + * finds all {@link Target#getControllerId()} which are currently in the + * database. + * + * @return all IDs of all {@link Target} in the system + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public List findAllTargetIds() { + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + final CriteriaQuery query = cb.createQuery(TargetIdName.class); + final Root targetRoot = query.from(Target.class); + return entityManager.createQuery( + query.multiselect(targetRoot.get(Target_.id), targetRoot.get(Target_.controllerId), + targetRoot.get(Target_.name))).getResultList(); + + } + + /** + * finds all {@link Target#getControllerId()} for all the given parameters. + * + * @param pageRequest + * the pageRequest to enhance the query for paging and sorting + * @param filterByDistributionId + * to find targets having the {@link DistributionSet} as + * installed or assigned. Set to null in case this + * is not required. + * @param filterByStatus + * find targets having this {@link TargetUpdateStatus}s. Set to + * null in case this is not required. + * @param filterBySearchText + * to find targets having the text anywhere in name or + * description. Set null in case this is not + * required. + * @param filterByTagNames + * to find targets which are having any one in this tag names. + * Set null in case this is not required. + * @param selectTargetWithNoTag + * flag to select targets with no tag assigned + * + * @return the found {@link Target}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public List findAllTargetIdsByFilters(final PageRequest pageRequest, + final Long filterByDistributionId, final Collection filterByStatus, + final String filterBySearchText, final Boolean selectTargetWithNoTag, final String... filterByTagNames) { + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + final CriteriaQuery query = cb.createQuery(Object[].class); + final Root targetRoot = query.from(Target.class); + List resultList = null; + final CriteriaQuery multiselect = query.multiselect(targetRoot.get(Target_.id), + targetRoot.get(Target_.controllerId), targetRoot.get(Target_.name), + targetRoot.get(pageRequest.getSort().iterator().next().getProperty())); + + final Predicate[] specificationsForMultiSelect = specificationsToPredicate( + buildSpecificationList(filterByStatus, filterBySearchText, filterByDistributionId, + selectTargetWithNoTag, false, filterByTagNames), targetRoot, multiselect, cb); + + // if we have some predicates then add it to the where clause of the + // multiselect + if (specificationsForMultiSelect.length > 0) { + multiselect.where(specificationsForMultiSelect); + } + + if (pageRequest.getSort() != null) { + final List orders = new ArrayList<>(); + final Sort sort = pageRequest.getSort(); + for (final Sort.Order sortOrder : sort) { + if (sortOrder.getDirection() == Direction.ASC) { + orders.add(cb.asc(targetRoot.get(sortOrder.getProperty()))); + } else { + orders.add(cb.desc(targetRoot.get(sortOrder.getProperty()))); + } + } + multiselect.orderBy(orders); + resultList = entityManager.createQuery(multiselect).setFirstResult(pageRequest.getOffset()) + .setMaxResults(pageRequest.getPageSize()).getResultList(); + } else { + resultList = entityManager.createQuery(multiselect).getResultList(); + } + return resultList.parallelStream().map(o -> new TargetIdName((long) o[0], o[1].toString(), o[2].toString())) + .collect(Collectors.toList()); + + } + + @PreDestroy + void destroy() { + eventBus.unregister(this); + } + + /** + * creating new {@link Target}s including poll status data. useful + * especially in plug and play scenarios. + * + * @param target + * to be created * + * @param status + * of the target + * @param lastTargetQuery + * if a plug and play case + * @param address + * if a plug and play case + * + * @throws EntityAlreadyExistsException + * + * @return + */ + @Modifying + @Transactional + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_TARGET + SpringEvalExpressions.HAS_AUTH_OR + + SpringEvalExpressions.IS_CONTROLLER) + @CacheEvict(value = { "targetsCreatedOverPeriod" }, allEntries = true) + public Target createTarget(@NotNull final Target target, @NotNull final TargetUpdateStatus status, + final Long lastTargetQuery, final URI address) { + + if (targetRepository.findByControllerId(target.getControllerId()) != null) { + throw new EntityAlreadyExistsException(target.getControllerId()); + } + + target.setNew(true); + final Target savedTarget = targetRepository.save(target); + final TargetInfo targetInfo = savedTarget.getTargetInfo(); + targetInfo.setUpdateStatus(status); + if (lastTargetQuery != null) { + targetInfo.setLastTargetQuery(lastTargetQuery); + } + if (address != null) { + targetInfo.setAddress(address.toString()); + } + targetInfo.setNew(true); + final Target targetToReturn = targetInfoRepository.save(targetInfo).getTarget(); + targetInfo.setNew(false); + return targetToReturn; + + } + + /** + * creating a new {@link Target}. + * + * @param target + * to be created + * @return the created {@link Target} + * + */ + @Modifying + @Transactional + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_TARGET + SpringEvalExpressions.HAS_AUTH_OR + + SpringEvalExpressions.IS_CONTROLLER) + @CacheEvict(value = { "targetsCreatedOverPeriod" }, allEntries = true) + public Target createTarget(@NotNull final Target target) { + return createTarget(target, TargetUpdateStatus.UNKNOWN, null, null); + } + + /** + * creates multiple {@link Target}s. If some of the given {@link Target}s + * already exists in the DB a {@link EntityAlreadyExistsException} is + * thrown. {@link Target}s contain all objects of the parameter targets, + * including duplicates. + * + * @param targets + * to be created. + * @return the created {@link Target}s + * + * @throws {@link EntityAlreadyExistsException} of one of the given targets + * already exist. + */ + @Modifying + @Transactional + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_TARGET) + public List createTargets(@NotNull final List targets) { + if (targetRepository.countByControllerIdIn(targets.stream().map(target -> target.getControllerId()) + .collect(Collectors.toList())) > 0) { + throw new EntityAlreadyExistsException(); + } + final List savedTargets = new ArrayList(); + for (final Target t : targets) { + final Target myTarget = createTarget(t); + savedTargets.add(myTarget); + } + return savedTargets; + } + + /** + * creating a new {@link Target} including poll status data. useful + * especially in plug and play scenarios. + * + * @param targets + * to be created * + * @param status + * of the target + * @param lastTargetQuery + * if a plug and play case + * @param address + * if a plug and play case + * + * @return newly created target + */ + @Modifying + @Transactional + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_TARGET) + public List createTargets(@NotNull final Collection targets, + @NotNull final TargetUpdateStatus status, final long lastTargetQuery, final URI address) { + if (targetRepository.countByControllerIdIn(targets.stream().map(target -> target.getControllerId()) + .collect(Collectors.toList())) > 0) { + throw new EntityAlreadyExistsException(); + } + final List savedTargets = new ArrayList(); + for (final Target t : targets) { + final Target myTarget = createTarget(t, status, lastTargetQuery, address); + savedTargets.add(myTarget); + } + return savedTargets; + } + + /** + * Find targets by tag name. + * + * @param tagName + * tag name + * @return list of matching targets + */ + @NotNull + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public List findTargetsByTag(@NotNull final String tagName) { + final TargetTag tag = targetTagRepository.findByNameEquals(tagName); + final List assignedTargets = targetRepository.findByTag(tag); + return assignedTargets; + } + + /** + * Count {@link TargetFilterQuery}s for given filter parameter. + * + * @param targetFilterQuery + * {link TargetFilterQuery} + * @return the found number {@link TargetFilterQuery}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Long countTargetByTargetFilterQuery(@NotNull final TargetFilterQuery targetFilterQuery) { + final Specification specs = RSQLUtility.parse(targetFilterQuery.getQuery(), TargetFields.class, + entityManager); + return targetRepository.count(specs); + } + + /** + * Count {@link TargetFilterQuery}s for given target filter query. + * + * @param targetFilterQuery + * {link TargetFilterQuery} + * @return the found number {@link TargetFilterQuery}s + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + public Long countTargetByTargetFilterQuery(@NotNull final String targetFilterQuery) { + final Specification specs = RSQLUtility.parse(targetFilterQuery, TargetFields.class, entityManager); + return targetRepository.count(specs); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetRepository.java new file mode 100755 index 000000000..6cb40cd09 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetRepository.java @@ -0,0 +1,281 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.Collection; +import java.util.List; + +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.Tag; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetTag; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; + +/** + * {@link Target} repository. + * + * + * + */ +@Transactional(readOnly = true) +public interface TargetRepository extends BaseEntityRepository, JpaSpecificationExecutor { + + /** + * Loads {@link Target} including details {@link EntityGraph} by given ID. + * + * @param controllerID + * to search for + * @return found {@link Target} or null if not found. + */ + @EntityGraph(value = "Target.detail", type = EntityGraphType.LOAD) + Target findByControllerId(String controllerID); + + /** + * Finds targets by given list of {@link Target#getControllerId()}s. + * + * @param controllerIDs + * to serach for + * @return list of found {@link Target}s + */ + List findByControllerIdIn(String... controllerIDs); + + /** + * Deletes the {@link Target}s with the given target IDs. + * + * @param targetIDs + * to be deleted + */ + @Modifying + @Transactional + // Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=349477 + @Query("DELETE FROM Target t WHERE t.id IN ?1") + void deleteByIdIn(final Collection targetIDs); + + /** + * Finds {@link Target}s by assigned {@link Tag}. + * + * @param tag + * to be found + * @return list of found targets + */ + @Query(value = "SELECT DISTINCT t FROM Target t JOIN t.tags tt WHERE tt = :tag") + List findByTag(@Param("tag") final TargetTag tag); + + /** + * Finds all {@link Target}s based on given {@link Target#getControllerId()} + * list and assigned {@link Tag#getName()}. + * + * @param tag + * to search for + * @param controllerIds + * to search for + * @return {@link List} of found {@link Target}s. + */ + @Query(value = "SELECT DISTINCT t from Target t JOIN t.tags tt WHERE tt.name = :tagname AND t.controllerId IN :targets") + List findByTagNameAndControllerIdIn(@Param("tagname") final String tag, + @Param("targets") final Collection controllerIds); + + /** + * Used by UI to filter based on selected status. + * + * @param pageable + * for page configuration + * @param status + * to filter for + * + * @return found targets + */ + Page findByTargetInfoUpdateStatus(final Pageable pageable, final TargetUpdateStatus status); + + /** + * Finds all targets that have defined {@link DistributionSet} installed. + * + * @param pageable + * for page configuration + * @param set + * is the {@link DistributionSet} to filter for. + * + * @return found targets + */ + Page findByTargetInfoInstalledDistributionSet(final Pageable pageable, final DistributionSet set); + + /** + * retrieves the {@link Target}s which has the {@link DistributionSet} + * installed with the given ID. + * + * @param pageable + * parameter + * @param setID + * the ID of the {@link DistributionSet} + * @return the found {@link Target}s + */ + Page findByTargetInfoInstalledDistributionSetId(final Pageable pageable, final Long setID); + + /** + * Finds all targets that have defined {@link DistributionSet} assigned. + * + * @param pageable + * for page configuration + * @param set + * is the {@link DistributionSet} to filter for. + * + * @return found targets + */ + Page findByAssignedDistributionSet(final Pageable pageable, final DistributionSet set); + + /** + * Saves all given {@link Target}s. + * + * @param entities + * @return the saved entities + * @throws IllegalArgumentException + * in case the given entity is (@literal null}. + * + * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable) + */ + @Override + @Modifying + @Transactional + @CacheEvict(value = { "targetStatus", "distributionUsageInstalled", "targetsLastPoll" }, allEntries = true) + List save(Iterable entities); + + /** + * Saves a given entity. Use the returned instance for further operations as + * the save operation might have changed the entity instance completely. + * + * @param entity + * the target to save + * @return the saved entity + */ + @Override + @Modifying + @Transactional + @CacheEvict(value = { "targetStatus", "distributionUsageInstalled", "targetsLastPoll" }, allEntries = true) + S save(S entity); + + /** + * Finds all targets that have defined {@link DistributionSet} assigned. + * + * @param pageable + * for page configuration + * @param setID + * is the ID of the {@link DistributionSet} to filter for. + * + * @return page of found targets + */ + Page findByAssignedDistributionSetId(final Pageable pageable, final Long setID); + + /** + * Counts number of targets with given + * {@link Target#getAssignedDistributionSet()}. + * + * @param distId + * to search for + * + * @return number of found {@link Target}s. + */ + Long countByAssignedDistributionSetId(final Long distId); + + /** + * @param ids + * of count in DB + * @return number of found {@link Target}s with given + * {@link Target#getControllerId()}s + */ + @Query("SELECT COUNT(t) FROM Target t WHERE t.controllerId IN ?1") + Long countByControllerIdIn(final Collection ids); + + /** + * Counts number of targets with given + * {@link TargetStatus#getInstalledDistributionSet()}. + * + * @param distId + * to search for + * @return number of found {@link Target}s. + */ + Long countByTargetInfoInstalledDistributionSetId(final Long distId); + + /** + * Finds all targets that have defined {@link DistributionSet} assigned or + * installed. + * + * @param pageable + * for page configuration + * @param assigned + * {@link DistributionSet} filter for; please note: must not be + * null + * @param installed + * {@link DistributionSet} filter for; please note: must not be + * null + * + * @return found targets + */ + Page findByAssignedDistributionSetOrTargetInfoInstalledDistributionSet(final Pageable pageable, + final DistributionSet assigned, final DistributionSet installed); + + /** + * Finds all targets that have defined {@link DistributionSet} assigned or + * installed. + * + * @param pageable + * for page configuration + * @param assigned + * {@link DistributionSet} filter for; please note: must not be + * null + * @param installed + * {@link DistributionSet} filter for; please note: must not be + * null + * @return found targets + */ + Page findByAssignedDistributionSetIdOrTargetInfoInstalledDistributionSetId(final Pageable pageable, + final Long assigned, final Long installed); + + /** + * Finds all {@link Target}s in the repository. + * + * @return {@link List} of {@link Target}s + * + * @see org.springframework.data.repository.CrudRepository#findAll() + */ + @Override + List findAll(); + + @Override + // Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=349477 + @Query("SELECT t FROM Target t WHERE t.id IN ?1") + List findAll(Iterable ids); + + /** + * Sets {@link Target#getAssignedDistributionSet()}. + * + * @param set + * to use + * @param modifiedAt + * current time + * @param modifiedBy + * current auditor + * @param targets + * to update + */ + @Modifying + @Transactional + @Query("UPDATE Target t SET t.assignedDistributionSet = :set, t.lastModifiedAt = :lastModifiedAt, t.lastModifiedBy = :lastModifiedBy WHERE t.id IN :targets") + void setAssignedDistributionSet(@Param("set") DistributionSet set, @Param("lastModifiedAt") Long modifiedAt, + @Param("lastModifiedBy") String modifiedBy, @Param("targets") Collection targets); +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetTagAssigmentResult.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetTagAssigmentResult.java new file mode 100755 index 000000000..10c1285b7 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetTagAssigmentResult.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.List; + +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetTag; + +/** + * Result object for {@link TargetTag} assigments. + * + * + * + * + */ +public class TargetTagAssigmentResult extends AssignmentResult { + + private final int unassigned; + private final List assignedTargets; + private final List unassignedTargets; + + /** + * Constructor. + * + * @param alreadyAssigned + * number of already assigned/ignored elements + * @param assigned + * number of newly assigned elements + * @param unassigned + * number of newly assigned elements + * @param assignedTargets + * {@link List} of assigned {@link Target}s. + * @param unassignedTargets + * {@link List} of unassigned {@link Target}s. + */ + public TargetTagAssigmentResult(final int alreadyAssigned, final int assigned, final int unassigned, + final List assignedTargets, final List unassignedTargets) { + super(assigned, alreadyAssigned); + this.unassigned = unassigned; + this.assignedTargets = assignedTargets; + this.unassignedTargets = unassignedTargets; + } + + /** + * @return the unassigned + */ + public int getUnassigned() { + return unassigned; + } + + /** + * @return the assignedTargets + */ + public List getAssignedTargets() { + return assignedTargets; + } + + /** + * @return the unassignedTargets + */ + public List getUnassignedTargets() { + return unassignedTargets; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetTagRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetTagRepository.java new file mode 100755 index 000000000..674631b0f --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetTagRepository.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.List; + +import org.eclipse.hawkbit.repository.model.TargetTag; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.transaction.annotation.Transactional; + +/** + * {@link TargetTag} repository. + * + * + * + */ +@Transactional(readOnly = true) +public interface TargetTagRepository extends BaseEntityRepository { + + /** + * deletes the {@link TargetTag}s with the given tag names. + * + * @param tagNames + * to be deleted + * @return 1 if tag was deleted + */ + @Modifying + @Transactional + Long deleteByName(final String tagName); + + /** + * find {@link TargetTag} by its name. + * + * @param tagName + * to filter on + * @return the {@link TargetTag} if found, otherwise null + */ + TargetTag findByNameEquals(final String tagName); + + /** + * Returns all instances of the type. + * + * @return all entities + */ + @Override + List findAll(); + + @Override + List save(Iterable entities); +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetWithActionType.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetWithActionType.java new file mode 100755 index 000000000..18efa23a9 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TargetWithActionType.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.ActionType; + +/** + * A custom JPA database return value object with the proper constructor so JPA + * is able to parse the result-set directly into this object. + * + * + * + */ +public class TargetWithActionType { + + private final String targetId; + private final ActionType actionType; + private final long forceTime; + + /** + * @param targetId + * @param actionType + * @param forceTime + */ + public TargetWithActionType(final String targetId, final ActionType actionType, final long forceTime) { + this.targetId = targetId; + this.actionType = actionType; + this.forceTime = forceTime; + } + + /** + * @return the actionType + */ + public ActionType getActionType() { + if (actionType != null) { + return actionType; + } + // default value + return ActionType.FORCED; + } + + /** + * @return the forceTime + */ + public long getForceTime() { + if (actionType == ActionType.TIMEFORCED) { + return forceTime; + } + return Action.NO_FORCE_TIME; + } + + /** + * @return the targetId + */ + public String getTargetId() { + return targetId; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TenantConfigurationRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TenantConfigurationRepository.java new file mode 100755 index 000000000..d8973f275 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TenantConfigurationRepository.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.List; + +import org.eclipse.hawkbit.repository.model.TenantConfiguration; +import org.springframework.transaction.annotation.Transactional; + +/** + * The spring-data repository for the entity {@link TenantConfiguration}. + * + * + * + * + */ +@Transactional(readOnly = true) +public interface TenantConfigurationRepository extends BaseEntityRepository { + + /** + * Finds a specific {@link TenantConfiguration} by the configuration key. + * + * @param configurationKey + * the configuration key to find the configuration for + * @return the found tenant configuration object otherwise {@code null} + */ + TenantConfiguration findByKey(String configurationKey); + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.CrudRepository#findAll() + */ + @Override + List findAll(); + + /** + * Deletes a tenant configuration by tenant and key. + * + * @param tenant + * the tenant for this configuration + * @param keyName + * the name of the key to be deleted + */ + void deleteByKey(String keyName); + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TenantKeyGenerator.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TenantKeyGenerator.java new file mode 100755 index 000000000..d0a2b4ea7 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TenantKeyGenerator.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.lang.reflect.Method; + +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.stereotype.Service; + +/** + * {@link KeyGenerator} for tenant related caches. + * + * + * + * + */ +@Service +public class TenantKeyGenerator implements KeyGenerator { + + @Autowired + private TenantAware tenantAware; + + /* + * (non-Javadoc) + * + * @see + * org.springframework.cache.interceptor.KeyGenerator#generate(java.lang. + * Object, java.lang.reflect.Method, java.lang.Object[]) + */ + @Override + public Object generate(final Object target, final Method method, final Object... params) { + return tenantAware.getCurrentTenant().toUpperCase(); + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TenantMetaDataRepository.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TenantMetaDataRepository.java new file mode 100755 index 000000000..08d672803 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/TenantMetaDataRepository.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.List; + +import org.eclipse.hawkbit.repository.model.TenantMetaData; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.transaction.annotation.Transactional; + +/** + * repository for operations on {@link TenantMetaData} entity. + * + * + * + * + */ +@Transactional(readOnly = true) +public interface TenantMetaDataRepository extends PagingAndSortingRepository { + + /** + * Search {@link TenantMetaData} by tenant name. + * + * @param tenant + * to search for + * @return found {@link TenantMetaData} or null + */ + TenantMetaData findByTenantIgnoreCase(String tenant); + + /** + * Counts the tenant by the tenant field which is either a count of one or a + * count of zero, this is mostly to check if the tenant exists. + * + * @param tenant + * the name of the tenant to check if it is exists + * @return the count of the tenant by name which is either one or zero + */ + Long countByTenantIgnoreCase(String tenant); + + @Override + List findAll(); + + /** + * @param tenant + */ + void deleteByTenantIgnoreCase(String tenant); + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/event/DeploymentManagementEvents.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/event/DeploymentManagementEvents.java new file mode 100755 index 000000000..30bec4471 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/event/DeploymentManagementEvents.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.event; + +import java.util.List; + +import org.eclipse.hawkbit.eventbus.event.CancelTargetAssignmentEvent; +import org.eclipse.hawkbit.eventbus.event.TargetAssignDistributionSetEvent; +import org.eclipse.hawkbit.eventbus.event.TargetInfoUpdateEvent; +import org.eclipse.hawkbit.repository.DeploymentManagement; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.google.common.eventbus.EventBus; + +/** + * Defines all events which are sent from the {@link DeploymentManagement}. + * + * + * + * + */ +@Service +public class DeploymentManagementEvents { + + @Autowired + private EventBus eventBus; + + /** + * Sends the {@link TargetAssignDistributionSetEvent} for a specific target + * to the {@link EventBus}. + * + * @param target + * the Target which has been assigned to a distribution set + * @param actionId + * the action id of the assignment + * @param softwareModules + * the software modules which have been assigned + */ + public void assignDistributionSet(final Target target, final Long actionId, + final List softwareModules) { + target.getTargetInfo().setUpdateStatus(TargetUpdateStatus.PENDING); + eventBus.post(new TargetInfoUpdateEvent(target.getTargetInfo())); + eventBus.post(new TargetAssignDistributionSetEvent(target.getControllerId(), actionId, softwareModules, + target.getTargetInfo().getAddress())); + } + + /** + * Sends the {@link CancelTargetAssignmentEvent} for a specific target to + * the {@link EventBus}. + * + * @param target + * the Target which has been assigned to a distribution set + * @param actionId + * the action id of the assignment + */ + public void cancalAssignDistributionSet(final Target target, final Long actionId) { + eventBus.post(new CancelTargetAssignmentEvent(target.getControllerId(), actionId, + target.getTargetInfo().getAddress())); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/ArtifactDeleteFailedException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/ArtifactDeleteFailedException.java new file mode 100755 index 000000000..7b7975d78 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/ArtifactDeleteFailedException.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * Thrown if artifact deletion failed. + * + * + * + * + */ +public final class ArtifactDeleteFailedException extends SpServerRtException { + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new FileUploadFailedException with + * {@link SpServerError#SP_REST_BODY_NOT_READABLE} error. + */ + public ArtifactDeleteFailedException() { + super(SpServerError.SP_ARTIFACT_DELETE_FAILED); + } + + /** + * @param cause + * for the exception + */ + public ArtifactDeleteFailedException(final Throwable cause) { + super(SpServerError.SP_ARTIFACT_DELETE_FAILED, cause); + } + + /** + * @param message + * of the error + */ + public ArtifactDeleteFailedException(final String message) { + super(message, SpServerError.SP_ARTIFACT_DELETE_FAILED); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/ArtifactUploadFailedException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/ArtifactUploadFailedException.java new file mode 100755 index 000000000..d83f80e64 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/ArtifactUploadFailedException.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * + * + * + */ +public final class ArtifactUploadFailedException extends SpServerRtException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new FileUploadFailedException with + * {@link SpServerError#SP_REST_BODY_NOT_READABLE} error. + */ + public ArtifactUploadFailedException() { + super(SpServerError.SP_ARTIFACT_UPLOAD_FAILED); + } + + /** + * @param cause + * for the exception + */ + public ArtifactUploadFailedException(final Throwable cause) { + super(SpServerError.SP_ARTIFACT_UPLOAD_FAILED, cause); + } + + /** + * @param message + * of the error + */ + public ArtifactUploadFailedException(final String message) { + super(message, SpServerError.SP_ARTIFACT_UPLOAD_FAILED); + } + + /** + * @param message + * for the error + * @param cause + * of the error + */ + public ArtifactUploadFailedException(final String message, final Throwable cause) { + super(message, SpServerError.SP_ARTIFACT_UPLOAD_FAILED, cause); + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/CancelActionNotAllowedException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/CancelActionNotAllowedException.java new file mode 100755 index 000000000..0a787e5bb --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/CancelActionNotAllowedException.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * Thrown if cancelation of actions is performened where the action is not + * cancelable, e.g. the action is not active or is already a cancel action. + * + * + * + * + */ +public final class CancelActionNotAllowedException extends SpServerRtException { + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new CancelActionNotAllowed with + * {@link SpServerError#SP_ACTION_NOT_CANCELABLE} error. + */ + public CancelActionNotAllowedException() { + super(SpServerError.SP_ACTION_NOT_CANCELABLE); + } + + /** + * @param cause + * for the exception + */ + public CancelActionNotAllowedException(final Throwable cause) { + super(SpServerError.SP_ACTION_NOT_CANCELABLE, cause); + } + + /** + * @param message + * of the error + */ + public CancelActionNotAllowedException(final String message) { + super(message, SpServerError.SP_ACTION_NOT_CANCELABLE); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/ConcurrentModificationException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/ConcurrentModificationException.java new file mode 100755 index 000000000..c2a3b4456 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/ConcurrentModificationException.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * {@link ConcurrentModificationException} is thrown when a given entity in's + * actual and cannot be stored within the current session. Reason could be that + * it has been changed within another session. + * + * + * + */ +public class ConcurrentModificationException extends SpServerRtException { + private static final long serialVersionUID = 1L; + private static final SpServerError THIS_ERROR = SpServerError.SP_REPO_CONCURRENT_MODIFICATION; + + /** + * Constructor. + */ + public ConcurrentModificationException() { + super(THIS_ERROR); + } + + /** + * Parameterized constructor. + * + * @param cause + * of the exception + */ + public ConcurrentModificationException(final Throwable cause) { + super(THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * custom error message + * @param cause + * of the exception + */ + public ConcurrentModificationException(final String message, final Throwable cause) { + super(message, THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * custom error message + */ + public ConcurrentModificationException(final String message) { + super(message, THIS_ERROR); + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/DistributionSetCreationFailedMissingMandatoryModuleException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/DistributionSetCreationFailedMissingMandatoryModuleException.java new file mode 100755 index 000000000..0dc2f9752 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/DistributionSetCreationFailedMissingMandatoryModuleException.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * Thrown if DS creation failed. + * + * + * + * + */ +public final class DistributionSetCreationFailedMissingMandatoryModuleException extends SpServerRtException { + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new FileUploadFailedException with + * {@link SpServerError#SP_DS_CREATION_FAILED_MISSING_MODULE} error. + */ + public DistributionSetCreationFailedMissingMandatoryModuleException() { + super(SpServerError.SP_DS_CREATION_FAILED_MISSING_MODULE); + } + + /** + * @param cause + * for the exception + */ + public DistributionSetCreationFailedMissingMandatoryModuleException(final Throwable cause) { + super(SpServerError.SP_DS_CREATION_FAILED_MISSING_MODULE, cause); + } + + /** + * @param message + * of the error + */ + public DistributionSetCreationFailedMissingMandatoryModuleException(final String message) { + super(message, SpServerError.SP_DS_CREATION_FAILED_MISSING_MODULE); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/DistributionSetTypeUndefinedException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/DistributionSetTypeUndefinedException.java new file mode 100755 index 000000000..d3e10b213 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/DistributionSetTypeUndefinedException.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetType; + +/** + * Thrown if the user tries to assign modules to a {@link DistributionSet} that + * has to {@link DistributionSetType} defined. + * + * + * + * + */ +public class DistributionSetTypeUndefinedException extends SpServerRtException { + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new FileUploadFailedException with + * {@link SpServerError#SP_DS_TYPE_UNDEFINED} error. + */ + public DistributionSetTypeUndefinedException() { + super(SpServerError.SP_DS_TYPE_UNDEFINED); + } + + /** + * @param cause + * for the exception + */ + public DistributionSetTypeUndefinedException(final Throwable cause) { + super(SpServerError.SP_DS_TYPE_UNDEFINED, cause); + } + + /** + * @param message + * of the error + */ + public DistributionSetTypeUndefinedException(final String message) { + super(message, SpServerError.SP_DS_TYPE_UNDEFINED); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/EntityAlreadyExistsException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/EntityAlreadyExistsException.java new file mode 100755 index 000000000..a38ec5bb6 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/EntityAlreadyExistsException.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * the {@link EntityAlreadyExistsException} is thrown when a entity is tried to + * be saved which already exists or which violates unique key constraints. + * + * + * + */ +public class EntityAlreadyExistsException extends SpServerRtException { + + private static final long serialVersionUID = 1L; + private static final SpServerError THIS_ERROR = SpServerError.SP_REPO_ENTITY_ALRREADY_EXISTS; + + /** + * Default constructor. + */ + public EntityAlreadyExistsException() { + super(THIS_ERROR); + } + + /** + * Parameterized constructor. + * + * @param cause + * of the exception + */ + public EntityAlreadyExistsException(final Throwable cause) { + super(THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + * @param cause + * of the exception + */ + public EntityAlreadyExistsException(final String message, final Throwable cause) { + super(message, THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + */ + public EntityAlreadyExistsException(final String message) { + super(message, THIS_ERROR); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/EntityLockedException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/EntityLockedException.java new file mode 100755 index 000000000..1d55bdc34 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/EntityLockedException.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * The {@link EntityLockedException} is thrown when an entity has been locked by + * the server to prevent modification. + * + * + * + */ +public class EntityLockedException extends SpServerRtException { + + private static final long serialVersionUID = 1L; + private static final SpServerError THIS_ERROR = SpServerError.SP_ENTITY_LOCKED; + + /** + * Default constructor. + */ + public EntityLockedException() { + super(THIS_ERROR); + } + + /** + * Parameterized constructor. + * + * @param cause + * of the exception + */ + public EntityLockedException(final Throwable cause) { + super(THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + * @param cause + * of the exception + */ + public EntityLockedException(final String message, final Throwable cause) { + super(message, THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + */ + public EntityLockedException(final String message) { + super(message, THIS_ERROR); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/EntityNotFoundException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/EntityNotFoundException.java new file mode 100755 index 000000000..280436d3b --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/EntityNotFoundException.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * the {@link EntityNotFoundException} is thrown when a entity is tried find but + * not found. + * + * + * + */ +public class EntityNotFoundException extends SpServerRtException { + + private static final long serialVersionUID = 1L; + private static final SpServerError THIS_ERROR = SpServerError.SP_REPO_ENTITY_NOT_EXISTS; + + /** + * Default constructor. + */ + public EntityNotFoundException() { + super(THIS_ERROR); + } + + /** + * Parameterized constructor. + * + * @param cause + * of the exception + */ + public EntityNotFoundException(final Throwable cause) { + super(THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + * @param cause + * of the exception + */ + public EntityNotFoundException(final String message, final Throwable cause) { + super(message, THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + */ + public EntityNotFoundException(final String message) { + super(message, THIS_ERROR); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/EntityReadOnlyException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/EntityReadOnlyException.java new file mode 100755 index 000000000..7908c4db5 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/EntityReadOnlyException.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * the {@link EntityReadOnlyException} is thrown when a entity is in read only + * mode and a user tries to change it. + */ +public class EntityReadOnlyException extends SpServerRtException { + + private static final long serialVersionUID = 1L; + private static final SpServerError THIS_ERROR = SpServerError.SP_REPO_ENTITY_READ_ONLY; + + /** + * Default constructor. + */ + public EntityReadOnlyException() { + super(THIS_ERROR); + } + + /** + * Parameterized constructor. + * + * @param cause + * of the exception + */ + public EntityReadOnlyException(final Throwable cause) { + super(THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + * @param cause + * of the exception + */ + public EntityReadOnlyException(final String message, final Throwable cause) { + super(message, THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + */ + public EntityReadOnlyException(final String message) { + super(message, THIS_ERROR); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/GridFSDBFileNotFoundException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/GridFSDBFileNotFoundException.java new file mode 100755 index 000000000..d22fe2b6e --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/GridFSDBFileNotFoundException.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * + * + * + */ +public final class GridFSDBFileNotFoundException extends SpServerRtException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new FileUploadFailedException with + * {@link SpServerError#SP_REST_BODY_NOT_READABLE} error. + */ + public GridFSDBFileNotFoundException() { + super(SpServerError.SP_ARTIFACT_LOAD_FAILED); + } + + /** + * @param cause + * for the exception + */ + public GridFSDBFileNotFoundException(final Throwable cause) { + super(SpServerError.SP_ARTIFACT_LOAD_FAILED, cause); + } + + /** + * @param message + * of the error + */ + public GridFSDBFileNotFoundException(final String message) { + super(message, SpServerError.SP_ARTIFACT_LOAD_FAILED); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/IncompleteDistributionSetException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/IncompleteDistributionSetException.java new file mode 100755 index 000000000..51b95afbd --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/IncompleteDistributionSetException.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * Thrown if a distribution set is assigned to a a target that is incomplete + * (i.e. mandatory modules are missing). + * + * + * + * + */ +public final class IncompleteDistributionSetException extends SpServerRtException { + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new IncompleteDistributionSetException with + * {@link SpServerError#SP_DS_INCOMPLETE} error. + */ + public IncompleteDistributionSetException() { + super(SpServerError.SP_DS_INCOMPLETE); + } + + /** + * @param cause + * for the exception + */ + public IncompleteDistributionSetException(final Throwable cause) { + super(SpServerError.SP_DS_INCOMPLETE, cause); + } + + /** + * @param message + * of the error + */ + public IncompleteDistributionSetException(final String message) { + super(message, SpServerError.SP_DS_INCOMPLETE); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/InsufficientPermissionException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/InsufficientPermissionException.java new file mode 100755 index 000000000..e15ba43ee --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/InsufficientPermissionException.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * Exception which is thrown in case the current security context object does + * not hold a required authority/permission. + * + * + * + */ +public class InsufficientPermissionException extends SpServerRtException { + + private static final long serialVersionUID = 1L; + + /** + * creates new InsufficientPermissionException. + * + * @param cause + * the cause of the exception + */ + public InsufficientPermissionException(final Throwable cause) { + super(SpServerError.SP_INSUFFICIENT_PERMISSION, cause); + } + + /** + * creates new InsufficientPermissionException. + */ + public InsufficientPermissionException() { + this(null); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/InvalidMD5HashException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/InvalidMD5HashException.java new file mode 100755 index 000000000..c08673164 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/InvalidMD5HashException.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * Thrown if MD5 checksum check fails. + * + * + * + * + */ +public class InvalidMD5HashException extends SpServerRtException { + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new FileUploadFailedException with + * {@link SpServerError#SP_ARTIFACT_UPLOAD_FAILED_MD5_MATCH} error. + */ + public InvalidMD5HashException() { + super(SpServerError.SP_ARTIFACT_UPLOAD_FAILED_MD5_MATCH); + } + + /** + * @param message + * of the error + * @param cause + * for the exception + */ + public InvalidMD5HashException(final String message, final Throwable cause) { + super(message, SpServerError.SP_ARTIFACT_UPLOAD_FAILED_MD5_MATCH, cause); + } + + /** + * @param message + * of the error + */ + public InvalidMD5HashException(final String message) { + super(message, SpServerError.SP_ARTIFACT_UPLOAD_FAILED_MD5_MATCH); + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/InvalidSHA1HashException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/InvalidSHA1HashException.java new file mode 100755 index 000000000..e4136cd82 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/InvalidSHA1HashException.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * Thrown if SHA1 checksum check fails. + * + * + * + * + */ +public class InvalidSHA1HashException extends SpServerRtException { + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new FileUploadFailedException with + * {@link SpServerError#SP_ARTIFACT_UPLOAD_FAILED_SHA1_MATCH} error. + */ + public InvalidSHA1HashException() { + super(SpServerError.SP_ARTIFACT_UPLOAD_FAILED_SHA1_MATCH); + } + + /** + * @param message + * of the error + * @param cause + * for the exception + */ + public InvalidSHA1HashException(final String message, final Throwable cause) { + super(message, SpServerError.SP_ARTIFACT_UPLOAD_FAILED_SHA1_MATCH, cause); + } + + /** + * @param message + * of the error + */ + public InvalidSHA1HashException(final String message) { + super(message, SpServerError.SP_ARTIFACT_UPLOAD_FAILED_SHA1_MATCH); + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/TenantNotExistException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/TenantNotExistException.java new file mode 100755 index 000000000..95aab39a8 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/TenantNotExistException.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * the {@link TenantNotExistException} is thrown when e.g. a controller tries to + * register itself at SP as plug'n play target and the tenant specified in the + * URL for this target does not exist. To avoid that targets could register + * automatically new tenants. + * + * + * + */ +public class TenantNotExistException extends SpServerRtException { + + private static final long serialVersionUID = 1L; + private static final SpServerError THIS_ERROR = SpServerError.SP_REPO_TENANT_NOT_EXISTS; + + /** + * Default constructor. + */ + public TenantNotExistException() { + super(THIS_ERROR); + } + + /** + * Parameterized constructor. + * + * @param cause + * of the exception + */ + public TenantNotExistException(final Throwable cause) { + super(THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + * @param cause + * of the exception + */ + public TenantNotExistException(final String message, final Throwable cause) { + super(message, THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + */ + public TenantNotExistException(final String message) { + super(message, THIS_ERROR); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/ToManyAttributeEntriesException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/ToManyAttributeEntriesException.java new file mode 100755 index 000000000..78da0b9f3 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/ToManyAttributeEntriesException.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * Thrown if too many status entries have been inserted. + * + * + * + * + */ +public final class ToManyAttributeEntriesException extends SpServerRtException { + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new FileUploadFailedException with + * {@link SpServerError#SP_ATTRIBUTES_TO_MANY_ENTRIES} error. + */ + public ToManyAttributeEntriesException() { + super(SpServerError.SP_ATTRIBUTES_TO_MANY_ENTRIES); + } + + /** + * @param cause + * for the exception + */ + public ToManyAttributeEntriesException(final Throwable cause) { + super(SpServerError.SP_ATTRIBUTES_TO_MANY_ENTRIES, cause); + } + + /** + * @param message + * of the error + */ + public ToManyAttributeEntriesException(final String message) { + super(message, SpServerError.SP_ATTRIBUTES_TO_MANY_ENTRIES); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/ToManyStatusEntriesException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/ToManyStatusEntriesException.java new file mode 100755 index 000000000..5d4367924 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/ToManyStatusEntriesException.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * Thrown if too many status entries have been inserted. + * + * + * + * + */ +public final class ToManyStatusEntriesException extends SpServerRtException { + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new FileUploadFailedException with + * {@link SpServerError#SP_REST_BODY_NOT_READABLE} error. + */ + public ToManyStatusEntriesException() { + super(SpServerError.SP_ACTION_STATUS_TO_MANY_ENTRIES); + } + + /** + * @param cause + * for the exception + */ + public ToManyStatusEntriesException(final Throwable cause) { + super(SpServerError.SP_ACTION_STATUS_TO_MANY_ENTRIES, cause); + } + + /** + * @param message + * of the error + */ + public ToManyStatusEntriesException(final String message) { + super(message, SpServerError.SP_ACTION_STATUS_TO_MANY_ENTRIES); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/UnsupportedSoftwareModuleForThisDistributionSetException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/UnsupportedSoftwareModuleForThisDistributionSetException.java new file mode 100755 index 000000000..3726e3188 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/exception/UnsupportedSoftwareModuleForThisDistributionSetException.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.SoftwareModule; + +/** + * Thrown if user tries to add a {@link SoftwareModule} to a + * {@link DistributionSet} that is not defined by< the + * {@link DistributionSetType}. + * + * + * + * + */ +public class UnsupportedSoftwareModuleForThisDistributionSetException extends SpServerRtException { + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new UnsupportedSoftwareModuleForThisDistributionSetException + * with {@link SpServerError#SP_DS_MODULE_UNSUPPORTED} error. + */ + public UnsupportedSoftwareModuleForThisDistributionSetException() { + super(SpServerError.SP_DS_MODULE_UNSUPPORTED); + } + + /** + * @param cause + * for the exception + */ + public UnsupportedSoftwareModuleForThisDistributionSetException(final Throwable cause) { + super(SpServerError.SP_DS_MODULE_UNSUPPORTED, cause); + } + + /** + * @param message + * of the error + */ + public UnsupportedSoftwareModuleForThisDistributionSetException(final String message) { + super(message, SpServerError.SP_DS_MODULE_UNSUPPORTED); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/Action.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/Action.java new file mode 100755 index 000000000..375ffcf0d --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/Action.java @@ -0,0 +1,395 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.util.List; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.ConstraintMode; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedAttributeNode; +import javax.persistence.NamedEntityGraph; +import javax.persistence.NamedEntityGraphs; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.Transient; + +import org.eclipse.hawkbit.cache.CacheField; +import org.eclipse.hawkbit.cache.CacheKeys; +import org.eclipse.persistence.annotations.CascadeOnDelete; + +/** + *

+ * Applicable transition changes of the software {@link SoftwareModule} state of + * a {@link Target}, e.g. install, uninstall, update, start, stop, and + * preparations for the transition change, i.e. download. + *

+ * + *

+ * Actions are managed by the SP server (SPS) and applied to the edge controller + * by the SP controller (SPC). Actions may also be value added commands that are + * nor directly related to SP, e.g. factory reset. + *

+ * + * + * + * + * + */ +@Table(name = "sp_action", indexes = { @Index(name = "sp_idx_action_01", columnList = "tenant,distribution_set"), + @Index(name = "sp_idx_action_02", columnList = "tenant,target,active"), + @Index(name = "sp_idx_action_prim", columnList = "tenant,id") }) +@NamedEntityGraphs({ @NamedEntityGraph(name = "Action.ds", attributeNodes = { @NamedAttributeNode("distributionSet") }), + @NamedEntityGraph(name = "Action.all", attributeNodes = { @NamedAttributeNode("distributionSet"), + @NamedAttributeNode("target") }) }) +@Entity +public class Action extends BaseEntity implements Comparable { + private static final long serialVersionUID = 1L; + + /** + * indicating that target action has no force time {@link #hasForcedTime()}. + */ + public static final long NO_FORCE_TIME = 0L; + + /** + * the {@link DistributionSet} which should be installed by this action. + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "distribution_set", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_action_ds") ) + private DistributionSet distributionSet; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "target", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_action_target") ) + private Target target; + + @Column(name = "active") + private boolean active; + + @Column(name = "action_type", nullable = false) + @Enumerated(EnumType.STRING) + private ActionType actionType; + + @Column(name = "forced_time") + private long forcedTime; + + @Column(name = "status") + private Status status; + + @CascadeOnDelete + @OneToMany(mappedBy = "action", targetEntity = ActionStatus.class, fetch = FetchType.LAZY, cascade = { + CascadeType.REMOVE }) + private List actionStatus; + + /** + * Note: filled only in {@link Status#DOWNLOAD}. + */ + @Transient + @CacheField(key = CacheKeys.DOWNLOAD_PROGRESS_PERCENT) + private int downloadProgressPercent; + + /** + * @return the distributionSet + */ + public DistributionSet getDistributionSet() { + return distributionSet; + } + + /** + * @param distributionSet + * the distributionSet to set + */ + public void setDistributionSet(final DistributionSet distributionSet) { + this.distributionSet = distributionSet; + } + + public boolean isCancelingOrCanceled() { + return status == Status.CANCELING || status == Status.CANCELED; + } + + /** + * @param active + * the active to set + */ + public void setActive(final boolean active) { + this.active = active; + } + + /** + * @return the status + */ + public Status getStatus() { + return status; + } + + /** + * @param status + * the status to set + */ + public void setStatus(final Status status) { + this.status = status; + } + + /** + * @return the downloadProgressPercent + */ + public int getDownloadProgressPercent() { + return downloadProgressPercent; + } + + /** + * @param downloadProgressPercent + * the downloadProgressPercent to set + */ + public void setDownloadProgressPercent(final int downloadProgressPercent) { + this.downloadProgressPercent = downloadProgressPercent; + } + + /** + * @return the active + */ + public boolean isActive() { + return active; + } + + /** + * @param actionType + * the actionType to set + */ + public void setActionType(final ActionType actionType) { + this.actionType = actionType; + } + + /** + * @return the actionType + */ + public ActionType getActionType() { + return actionType; + } + + /** + * @return the actionStatus + */ + public List getActionStatus() { + return actionStatus; + } + + /** + * @param target + * the target to set + */ + public void setTarget(final Target target) { + this.target = target; + } + + /** + * @return the target + */ + public Target getTarget() { + return target; + } + + /** + * @return the forcedTime + */ + public long getForcedTime() { + return forcedTime; + } + + /** + * @param forcedTime + * the forcedTime to set + */ + public void setForcedTime(final long forcedTime) { + this.forcedTime = forcedTime; + } + + @Override + public int compareTo(final Action o) { + if (super.getId() == null || o == null || o.getId() == null) { + return 0; + } + return super.getId().compareTo(o.getId()); + } + + /** + * checks if the {@link #forcedTime} is hit by the given + * {@code hitTimeMillis}, by means if the given milliseconds are greater + * than the forcedTime. + * + * @param hitTimeMillis + * the milliseconds, mostly the + * {@link System#currentTimeMillis()} + * @return {@code true} if this {@link #type} is in + * {@link ActionType#TIMEFORCED} and the given {@code hitTimeMillis} + * is greater than the {@link #forcedTime} otherwise {@code false} + */ + public boolean isHitAutoForceTime(final long hitTimeMillis) { + if (actionType == ActionType.TIMEFORCED) { + return hitTimeMillis >= forcedTime; + } + return false; + } + + /** + * @return {@code true} if either the {@link #type} is + * {@link ActionType#FORCED} or {@link ActionType#TIMEFORCED} but + * then if the {@link #forcedTime} has been exceeded otherwise + * always {@code false} + */ + public boolean isForce() { + switch (actionType) { + case FORCED: + return true; + case TIMEFORCED: + return isHitAutoForceTime(System.currentTimeMillis()); + default: + return false; + } + } + + /** + * @return the forced + */ + public boolean isForced() { + return actionType == ActionType.FORCED; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Action [distributionSet=" + distributionSet + ", getId()=" + getId() + "]"; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((actionType == null) ? 0 : actionType.hashCode()); + result = prime * result + (active ? 1231 : 1237); + result = prime * result + (int) (forcedTime ^ (forcedTime >>> 32)); + result = prime * result + ((status == null) ? 0 : status.hashCode()); + result = prime * result + (isHitAutoForceTime(System.currentTimeMillis()) ? 1231 : 1237); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { // NOSONAR - as this is generated + // code + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Action other = (Action) obj; + if (actionType != other.actionType) { + return false; + } + if (active != other.active) { + return false; + } + if (forcedTime != other.forcedTime) { + return false; + } + if (status != other.status) { + return false; + } + return true; + } + + /** + * Action status as reported by the controller. + * + * Be aware that JPA is persiting the ordnial number of the enum by means + * the ordered number in the enum. So don't re-order the enums within the + * Status enum declaration! + * + * + * + * + * + */ + public enum Status { + /** + * Action is finished successfully for this target. + */ + FINISHED, + + /** + * Action has failed for this target. + */ + ERROR, + + /** + * Action is still running but with warnings. + */ + WARNING, + + /** + * Action is still running for this target. + */ + RUNNING, + + /** + * Action has been canceled for this target. + */ + CANCELED, + + /** + * Action is in canceling state and waiting for controller confirmation. + */ + CANCELING, + + /** + * Action has been presented to the target. + */ + RETRIEVED, + + /** + * Action needs download by this target which has now started. + */ + DOWNLOAD; + } + + /** + * The action type for this action relation. + * + * + * + * + */ + public enum ActionType { + FORCED, SOFT, TIMEFORCED; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/ActionStatus.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/ActionStatus.java new file mode 100755 index 000000000..28389ab01 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/ActionStatus.java @@ -0,0 +1,169 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.util.ArrayList; +import java.util.List; + +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ConstraintMode; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedAttributeNode; +import javax.persistence.NamedEntityGraph; +import javax.persistence.Table; + +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.persistence.annotations.CascadeOnDelete; + +import com.google.common.base.Splitter; + +/** + * Entity to store the status for a specific action. + * + * + * + */ +@Table(name = "sp_action_status", indexes = { @Index(name = "sp_idx_action_status_01", columnList = "tenant,action"), + @Index(name = "sp_idx_action_status_02", columnList = "tenant,action,status"), + @Index(name = "sp_idx_action_status_prim", columnList = "tenant,id") }) +@NamedEntityGraph(name = "ActionStatus.withMessages", attributeNodes = { @NamedAttributeNode("messages") }) +@Entity +public class ActionStatus extends BaseEntity { + + /** + * + */ + private static final long serialVersionUID = 1L; + + @Column(name = "target_occurred_at") + private Long occurredAt; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "action", nullable = false, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_act_stat_action") ) + private Action action; + + @Column(name = "status") + private Status status; + + @CascadeOnDelete + @ElementCollection(fetch = FetchType.LAZY, targetClass = String.class) + @CollectionTable(name = "sp_action_status_messages", joinColumns = @JoinColumn(name = "action_status_id", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_stat_msg_act_stat") ) , indexes = { + @Index(name = "sp_idx_action_status_msgs_01", columnList = "action_status_id") }) + @Column(name = "detail_message", length = 512) + private final List messages = new ArrayList(); + + /** + * Creates a new {@link ActionStatus} object. + * + * @param action + * the action for this action status + * @param status + * the status for this action status + * @param occurredAt + * the occurred timestamp + */ + public ActionStatus(final Action action, final Status status, final Long occurredAt) { + this.action = action; + this.status = status; + this.occurredAt = occurredAt; + } + + /** + * Creates a new {@link ActionStatus} object. + * + * @param action + * the action for this action status + * @param status + * the status for this action status + * @param occurredAt + * the occurred timestamp + * @param messages + * the messages which should be added to this action status + */ + public ActionStatus(final Action action, final Status status, final Long occurredAt, final String... messages) { + this.action = action; + this.status = status; + this.occurredAt = occurredAt; + for (final String msg : messages) { + addMessage(msg); + } + } + + /** + * + */ + public ActionStatus() { + } + + /** + * @return the occurredAt + */ + public Long getOccurredAt() { + return occurredAt; + } + + /** + * @param occurredAt + * the occurredAt to set + */ + public void setOccurredAt(final Long occurredAt) { + this.occurredAt = occurredAt; + } + + /** + * Adds message. + * + * @param message + * to add + */ + public final void addMessage(final String message) { + Splitter.fixedLength(512).split(message).forEach(chunk -> messages.add(chunk)); + } + + public List getMessages() { + return messages; + } + + /** + * @return the action + */ + public Action getAction() { + return action; + } + + /** + * @param action + * the action to set + */ + public void setAction(final Action action) { + this.action = action; + } + + /** + * @return the status + */ + public Status getStatus() { + return status; + } + + /** + * @param status + * the status to set + */ + public void setStatus(final Status status) { + this.status = status; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/ActionWithStatusCount.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/ActionWithStatusCount.java new file mode 100755 index 000000000..abe2ee961 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/ActionWithStatusCount.java @@ -0,0 +1,169 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.Action.Status; + +/** + * Custom JPA Model for querying {@link Action} include the count of the + * action's {@link ActionStatus}. + * + * + * + * + */ +public class ActionWithStatusCount { + private final Long actionStatusCount; + private final Long actionId; + private final ActionType actionType; + private final boolean actionActive; + private final long actionForceTime; + private final Status actionStatus; + private final Long actionCreatedAt; + private final Long actionLastModifiedAt; + private final Long dsId; + private final String dsName; + private final String dsVersion; + private final Action action; + + /** + * JPA constructor, the parameter are the result set columns of the custom + * query. + * + * @param actionId + * the ID of the action + * @param actionType + * the action type + * @param active + * the active attribute of the action + * @param forcedTime + * the forced time attribute of the action + * @param status + * the status attribute of the action + * @param actionCreatedAt + * the createdAt timestamp of the action + * @param actionLastModifiedAt + * the last modified timestamp of the action + * @param dsId + * the ID of the distributionset + * @param dsName + * the name of the distributionset + * @param dsVersion + * the version of the distributionset + * @param actionStatusCount + * the count of the action status for this action + */ + public ActionWithStatusCount(final Long actionId, final ActionType actionType, final boolean active, + final long forcedTime, final Status status, final Long actionCreatedAt, final Long actionLastModifiedAt, + final Long dsId, final String dsName, final String dsVersion, final Long actionStatusCount) { + this.actionId = actionId; + this.actionType = actionType; + this.actionActive = active; + this.actionForceTime = forcedTime; + this.actionStatus = status; + this.actionCreatedAt = actionCreatedAt; + this.actionLastModifiedAt = actionLastModifiedAt; + this.dsId = dsId; + this.dsName = dsName; + this.dsVersion = dsVersion; + this.actionStatusCount = actionStatusCount; + + this.action = new Action(); + this.action.setActionType(actionType); + this.action.setActive(actionActive); + this.action.setForcedTime(actionForceTime); + this.action.setStatus(actionStatus); + this.action.setId(actionId); + } + + /** + * @return the action + */ + public Action getAction() { + return action; + } + + /** + * @return the actionId + */ + public Long getActionId() { + return actionId; + } + + /** + * @return the actionType + */ + public ActionType getActionType() { + return actionType; + } + + /** + * @return the actionActive + */ + public boolean isActionActive() { + return actionActive; + } + + /** + * @return the actionForceTime + */ + public long getActionForceTime() { + return actionForceTime; + } + + /** + * @return the actionStatus + */ + public Status getActionStatus() { + return actionStatus; + } + + /** + * @return the actionCreatedAt + */ + public Long getActionCreatedAt() { + return actionCreatedAt; + } + + /** + * @return the actionLastModifiedAt + */ + public Long getActionLastModifiedAt() { + return actionLastModifiedAt; + } + + /** + * @return the dsId + */ + public Long getDsId() { + return dsId; + } + + /** + * @return the dsName + */ + public String getDsName() { + return dsName; + } + + /** + * @return the dsVersion + */ + public String getDsVersion() { + return dsVersion; + } + + /** + * @return the actionStatusCount + */ + public Long getActionStatusCount() { + return actionStatusCount; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/Artifact.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/Artifact.java new file mode 100755 index 000000000..4b868f9a2 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/Artifact.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; + +/** + * Tenant specific locally stored artifact representation that is used by + * {@link SoftwareModule} . + * + * + * + * + */ +@MappedSuperclass +public abstract class Artifact extends BaseEntity { + private static final long serialVersionUID = 1L; + + @Column(name = "sha1_hash", length = 40, nullable = true) + private String sha1Hash; + + @Column(name = "md5_hash", length = 32, nullable = true) + private String md5Hash; + + @Column(name = "file_size") + private Long size; + + public abstract SoftwareModule getSoftwareModule(); + + /** + * @return the md5Hash + */ + public String getMd5Hash() { + return md5Hash; + } + + /** + * @return the sha1Hash + */ + public String getSha1Hash() { + return sha1Hash; + } + + /** + * @param md5Hash + * the md5Hash to set + */ + public void setMd5Hash(final String md5Hash) { + this.md5Hash = md5Hash; + } + + /** + * @param sha1Hash + * the sha1Hash to set + */ + public void setSha1Hash(final String sha1Hash) { + this.sha1Hash = sha1Hash; + } + + /** + * @return the size + */ + public Long getSize() { + return size; + } + + /** + * @param size + * the size to set + */ + public void setSize(final Long size) { + this.size = size; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/BaseEntity.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/BaseEntity.java new file mode 100755 index 000000000..cd9873fe8 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/BaseEntity.java @@ -0,0 +1,258 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.io.Serializable; + +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; +import javax.persistence.PrePersist; +import javax.persistence.Version; + +import org.eclipse.hawkbit.eventbus.CacheFieldEntityListener; +import org.eclipse.hawkbit.repository.exception.TenantNotExistException; +import org.eclipse.hawkbit.repository.model.helper.SystemManagementHolder; +import org.eclipse.hawkbit.repository.model.helper.TenantAwareHolder; +import org.eclipse.persistence.annotations.Multitenant; +import org.eclipse.persistence.annotations.MultitenantType; +import org.eclipse.persistence.annotations.TenantDiscriminatorColumn; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.hateoas.Identifiable; + +/** + * Holder of all base attributes common to all entities. + * + * + * + * + * + */ +@MappedSuperclass +@Access(AccessType.FIELD) +@EntityListeners({ AuditingEntityListener.class, CacheFieldEntityListener.class }) +@TenantDiscriminatorColumn(name = "tenant", length = 40) +@Multitenant(MultitenantType.SINGLE_TABLE) +public abstract class BaseEntity implements Serializable, Identifiable { + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + private String createdBy; + private String lastModifiedBy; + private Long createdAt; + private Long lastModifiedAt; + + @Version + @Column(name = "optlock_revision") + private long optLockRevision; + + @Column(name = "tenant", nullable = false, insertable = false, updatable = false, length = 40) + private String tenant; + + /** + * + */ + public BaseEntity() { + + } + + /** + * @param entity + * the entity to copy + */ + public BaseEntity(final BaseEntity entity) { + id = entity.id; + createdAt = entity.createdAt; + createdBy = entity.createdBy; + lastModifiedAt = entity.lastModifiedAt; + lastModifiedBy = entity.lastModifiedBy; + optLockRevision = entity.optLockRevision; + } + + /** + * PrePersist listener method for all {@link BaseEntity} entities. + */ + @PrePersist + public void prePersist() { + // before persisting the entity check the current ID of the tenant by + // using the TenantAware + // service + final String currentTenant = SystemManagementHolder.getInstance().currentTenant(); + if (currentTenant == null) { + throw new TenantNotExistException( + "Tenant " + TenantAwareHolder.getInstance().getTenantAware().getCurrentTenant() + + " does not exists, cannot create entity " + this.getClass() + " with id " + id); + } + setTenant(currentTenant.toUpperCase()); + } + + @Access(AccessType.PROPERTY) + @Column(name = "created_at", insertable = true, updatable = false) + public Long getCreatedAt() { + return createdAt; + } + + @Access(AccessType.PROPERTY) + @Column(name = "created_by", insertable = true, updatable = false, length = 40) + public String getCreatedBy() { + return createdBy; + } + + @Access(AccessType.PROPERTY) + @Column(name = "last_modified_at", insertable = false, updatable = true) + public Long getLastModifiedAt() { + return lastModifiedAt; + } + + @Access(AccessType.PROPERTY) + @Column(name = "last_modified_by", insertable = false, updatable = true, length = 40) + public String getLastModifiedBy() { + return lastModifiedBy; + } + + /** + * @param createdBy + * the createdBy to set + */ + @CreatedBy + public void setCreatedBy(final String createdBy) { + this.createdBy = createdBy; + } + + /** + * @param lastModifiedBy + * the lastModifiedBy to set + */ + @LastModifiedBy + public void setLastModifiedBy(final String lastModifiedBy) { + this.lastModifiedBy = lastModifiedBy; + } + + /** + * @param createdAt + * the createdAt to set + */ + @CreatedDate + public void setCreatedAt(final Long createdAt) { + this.createdAt = createdAt; + } + + /** + * @param lastModifiedAt + * the lastModifiedAt to set + */ + @LastModifiedDate + public void setLastModifiedAt(final Long lastModifiedAt) { + this.lastModifiedAt = lastModifiedAt; + } + + public long getOptLockRevision() { + return optLockRevision; + } + + /** + * @return the tenant + */ + public String getTenant() { + return tenant; + } + + /** + * @param tenant + * the tenant to set + */ + public void setTenant(final String tenant) { + this.tenant = tenant; + } + + /** + * @return the id + */ + @Override + public Long getId() { + return id; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "BaseEntity [id=" + id + "]"; + } + + /** + * @param id + * the id to set + */ + public void setId(final Long id) { + this.id = id; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { // NOSONAR - as this is generated code + final int prime = 31; + int result = 1; + result = prime * result + (id == null ? 0 : id.hashCode()); + result = prime * result + (int) (optLockRevision ^ optLockRevision >>> 32); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { // NOSONAR - as this is generated + // code + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final BaseEntity other = (BaseEntity) obj; + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + if (optLockRevision != other.optLockRevision) { + return false; + } + return true; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/CustomSoftwareModule.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/CustomSoftwareModule.java new file mode 100755 index 000000000..96c7cdd62 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/CustomSoftwareModule.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.io.Serializable; + +/** + * Use to display software modules for the selected distribution. + * + * + * + * + */ +public class CustomSoftwareModule implements Serializable { + + private static final long serialVersionUID = 6144585781451168439L; + + private final SoftwareModule softwareModule; + + private final boolean assigned; + + /** + * Constructor. + * + * @param softwareModule + * entity. + * @param assigned + * as true if the software module is assigned and false if not + * assigned. + */ + public CustomSoftwareModule(final SoftwareModule softwareModule, final boolean assigned) { + this.softwareModule = softwareModule; + this.assigned = assigned; + } + + /** + * @return the softwareModule + */ + public SoftwareModule getSoftwareModule() { + return softwareModule; + } + + /** + * @return the assigned + */ + public boolean isAssigned() { + return assigned; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSet.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSet.java new file mode 100755 index 000000000..1f5843308 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSet.java @@ -0,0 +1,362 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.ConstraintMode; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; +import javax.persistence.NamedAttributeNode; +import javax.persistence.NamedEntityGraph; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +import org.eclipse.hawkbit.repository.exception.DistributionSetTypeUndefinedException; +import org.eclipse.hawkbit.repository.exception.UnsupportedSoftwareModuleForThisDistributionSetException; +import org.eclipse.persistence.annotations.CascadeOnDelete; + +/** + *

+ * The {@link DistributionSet} is defined in the SP repository and contains at + * least an OS and an Agent Hub. + *

+ * + *

+ * A {@link Target} has exactly one target {@link DistributionSet} assigned. + *

+ * + * + * + * + * + */ +@Entity +@Table(name = "sp_distribution_set", uniqueConstraints = { + @UniqueConstraint(columnNames = { "name", "version", "tenant" }, name = "uk_distrib_set") }, indexes = { + @Index(name = "sp_idx_distribution_set_01", columnList = "tenant,deleted,name,complete"), + @Index(name = "sp_idx_distribution_set_02", columnList = "tenant,required_migration_step"), + @Index(name = "sp_idx_distribution_set_prim", columnList = "tenant,id") }) +@NamedEntityGraph(name = "DistributionSet.detail", attributeNodes = { @NamedAttributeNode("modules"), + @NamedAttributeNode("tags"), @NamedAttributeNode("type") }) +public class DistributionSet extends NamedVersionedEntity { + private static final long serialVersionUID = 1L; + + @Column(name = "required_migration_step") + private boolean requiredMigrationStep = false; + + @ManyToMany(targetEntity = SoftwareModule.class, fetch = FetchType.LAZY) + @JoinTable(name = "sp_ds_module", joinColumns = { + @JoinColumn(name = "ds_id", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_ds_module_ds") ) }, inverseJoinColumns = { + @JoinColumn(name = "module_id", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_ds_module_module") ) }) + private final Set modules = new HashSet(); + + @ManyToMany(targetEntity = DistributionSetTag.class) + @JoinTable(name = "sp_ds_dstag", joinColumns = { + @JoinColumn(name = "ds", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_ds_dstag_ds") ) }, inverseJoinColumns = { + @JoinColumn(name = "TAG", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_ds_dstag_tag") ) }) + private Set tags = new HashSet(); + + @Column(name = "deleted") + private boolean deleted = false; + + @OneToMany(mappedBy = "assignedDistributionSet", targetEntity = Target.class, fetch = FetchType.LAZY) + private List assignedToTargets; + + @OneToMany(mappedBy = "installedDistributionSet", targetEntity = TargetInfo.class, fetch = FetchType.LAZY) + private List installedAtTargets; + + @OneToMany(mappedBy = "distributionSet", targetEntity = Action.class, fetch = FetchType.LAZY) + private List actions; + + @CascadeOnDelete + @OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.REMOVE }) + @JoinColumn(name = "ds_id", insertable = false, updatable = false) + private final List metadata = new ArrayList<>(); + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ds_id", nullable = false, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_ds_dstype_ds") ) + private DistributionSetType type; + + @Column(name = "complete") + private boolean complete = false; + + /** + * Default constructor. + */ + public DistributionSet() { + super(); + } + + /** + * Parameterized constructor. + * + * @param name + * of the {@link DistributionSet} + * @param version + * of the {@link DistributionSet} + * @param description + * of the {@link DistributionSet} + * @param type + * of the {@link DistributionSet} + * @param moduleList + * {@link SoftwareModule}s of the {@link DistributionSet} + */ + public DistributionSet(final String name, final String version, final String description, + final DistributionSetType type, final Iterable moduleList) { + super(name, version, description); + + this.type = type; + if (moduleList != null) { + moduleList.forEach(module -> addModule(module)); + } + complete = type.checkComplete(this); + } + + public Set getTags() { + return tags; + } + + /** + * @return the deleted + */ + public boolean isDeleted() { + return deleted; + } + + /** + * @return the metadata + */ + public List getMetadata() { + return metadata; + } + + /** + * @return the actions + */ + public List getActions() { + return actions; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + this.getClass().getName().hashCode(); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof DistributionSet)) { + return false; + } + + return true; + } + + public boolean isRequiredMigrationStep() { + return requiredMigrationStep; + } + + /** + * @param deleted + * the deleted to set + */ + public DistributionSet setDeleted(final boolean deleted) { + this.deleted = deleted; + return this; + } + + public DistributionSet setRequiredMigrationStep(final boolean isRequiredMigrationStep) { + requiredMigrationStep = isRequiredMigrationStep; + return this; + } + + /** + * @param tags + * the tags to set + */ + public DistributionSet setTags(final Set tags) { + this.tags = tags; + return this; + } + + /** + * @return the assignedTargets + */ + public List getAssignedTargets() { + return assignedToTargets; + } + + /** + * @return the installedTargets + */ + public List getInstalledTargets() { + return installedAtTargets; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "DistributionSet [getName()=" + getName() + ", getOptLockRevision()=" + getOptLockRevision() + + ", getId()=" + getId() + "]"; + } + + /** + * + * @return unmodifiableSet of {@link SoftwareModule}. + */ + public Set getModules() { + return Collections.unmodifiableSet(modules); + } + + public DistributionSetIdName getDistributionSetIdName() { + return new DistributionSetIdName(getId(), getName(), getVersion()); + } + + /** + * @param softwareModule + * @return true if the module was added and false + * if it already existed in the set + * + */ + public boolean addModule(final SoftwareModule softwareModule) { + + // we cannot allow that modules are added without a type defined + if (type == null) { + throw new DistributionSetTypeUndefinedException(); + } + + // check if it is allowed to such a module to this DS type + if (!type.containsModuleType(softwareModule.getType())) { + throw new UnsupportedSoftwareModuleForThisDistributionSetException(); + } + + final Optional found = modules.stream() + .filter(module -> module.getId().equals(softwareModule.getId())).findFirst(); + + if (found.isPresent()) { + return false; + } + + final long allready = modules.stream() + .filter(module -> module.getType().getKey().equals(softwareModule.getType().getKey())).count(); + + if (allready >= softwareModule.getType().getMaxAssignments()) { + final Optional sameKey = modules.stream() + .filter(module -> module.getType().getKey().equals(softwareModule.getType().getKey())).findFirst(); + modules.remove(sameKey.get()); + } + + if (modules.add(softwareModule)) { + complete = type.checkComplete(this); + return true; + } + + return false; + } + + /** + * Removed given {@link SoftwareModule} from this DS instance. + * + * @param softwareModule + * to remove + * @return true if element was found and removed + */ + public boolean removeModule(final SoftwareModule softwareModule) { + final Optional found = modules.stream() + .filter(module -> module.getId().equals(softwareModule.getId())).findFirst(); + + if (found.isPresent()) { + modules.remove(found.get()); + complete = type.checkComplete(this); + return true; + } + + return false; + + } + + /** + * Searches through modules for the given type. + * + * @param type + * to serach for + * @return SoftwareModule of giben type or null if not in the + * list. + */ + public SoftwareModule findFirstModuleByType(final SoftwareModuleType type) { + final Optional result = modules.stream().filter(module -> module.getType().equals(type)) + .findFirst(); + + if (result.isPresent()) { + return result.get(); + } + + return null; + } + + /** + * @return the type + */ + public DistributionSetType getType() { + return type; + } + + /** + * @param type + * the type to set + */ + public void setType(final DistributionSetType type) { + this.type = type; + } + + /** + * @return the complete + */ + public boolean isComplete() { + return complete; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetIdName.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetIdName.java new file mode 100755 index 000000000..a7eb8e517 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetIdName.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.io.Serializable; + +/** + * + * + */ +public class DistributionSetIdName implements Serializable { + /** + * + */ + private static final long serialVersionUID = 1L; + + private final Long id; + private final String name; + private final String version; + + /** + * @param id + * the {@link DistributionSet#getId()} + * @param name + * the {@link DistributionSet#getName()} + * @param version + * the {@link DistributionSet#getVersion()} + * + */ + public DistributionSetIdName(final Long id, final String name, final String version) { + this.id = id; + this.name = name; + this.version = version; + } + + /** + * @return the id + */ + public Long getId() { + return id; + } + + public String getVersion() { + return version; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { // NOSONAR - as this is generated + final int prime = 31; + int result = 1; + result = prime * result + (id == null ? 0 : id.hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { // NOSONAR - as this is generated + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final DistributionSetIdName other = (DistributionSetIdName) obj; + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + return true; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + // only return the ID because it's used in vaadin for setting the item + // id in the dom + return id.toString(); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetMetadata.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetMetadata.java new file mode 100755 index 000000000..b0627f0db --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetMetadata.java @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.io.Serializable; + +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.ConstraintMode; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +/** + * Metadata for {@link DistributionSet}. + * + * + * + * + */ +@IdClass(DsMetadataCompositeKey.class) +@Entity +@Table(name = "sp_ds_metadata") +public class DistributionSetMetadata implements Serializable { + + /** + * + */ + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "meta_key", length = 128) + private String key; + + @Column(name = "meta_value", length = 4000) + @Basic + private String value; + + @Id + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ds_id", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_metadata_ds") ) + private DistributionSet distributionSet; + + public DistributionSetMetadata() { + + } + + /** + * Parameter constructor. + * + * @param key + * @param distributionSet + * @param value + */ + public DistributionSetMetadata(final String key, final DistributionSet distributionSet, final String value) { + this.key = key; + this.distributionSet = distributionSet; + this.value = value; + } + + public DsMetadataCompositeKey getId() { + return new DsMetadataCompositeKey(distributionSet, key); + } + + /** + * @return the key + */ + public String getKey() { + return key; + } + + /** + * @param key + * the key to set + */ + public void setKey(final String key) { + this.key = key; + } + + /** + * @param distributionSet + * the distributionSet to set + */ + public void setDistributionSet(final DistributionSet distributionSet) { + this.distributionSet = distributionSet; + } + + /** + * @return the value + */ + public String getValue() { + return value; + } + + /** + * @param value + * the value to set + */ + public void setValue(final String value) { + this.value = value; + } + + /** + * @return the distributionSet + */ + public DistributionSet getDistributionSet() { + return distributionSet; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetTag.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetTag.java new file mode 100755 index 000000000..0e28caa7c --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetTag.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * A {@link DistributionSetTag} is used to describe DistributionSet attributes + * and use them also for filtering the DistributionSet list. + * + * + * + * + * + * + */ +@Entity +@Table(name = "sp_distributionset_tag", indexes = { + @Index(name = "sp_idx_distribution_set_tag_prim", columnList = "tenant,id") }, uniqueConstraints = @UniqueConstraint(columnNames = { + "name", "tenant" }, name = "uk_ds_tag") ) +public class DistributionSetTag extends Tag { + private static final long serialVersionUID = 1L; + + /** + * Public constructor. + * + * @param name + * of the {@link DistributionSetTag} + **/ + public DistributionSetTag(final String name) { + super(name, null, null); + } + + /** + * Public constructor. + * + * @param name + * of the {@link DistributionSetTag} + * @param description + * of the {@link DistributionSetTag} + * @param colour + * of tag in UI + */ + public DistributionSetTag(final String name, final String description, final String colour) { + super(name, description, colour); + } + + DistributionSetTag() { + super(); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + this.getClass().getName().hashCode(); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof DistributionSetTag)) { + return false; + } + + return true; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetType.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetType.java new file mode 100755 index 000000000..47bc4b1b7 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetType.java @@ -0,0 +1,285 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * A distribution set type defines which software module types can or have to be + * {@link DistributionSet}. + * + * + * + * + */ +@Entity +@Table(name = "sp_distribution_set_type", indexes = { + @Index(name = "sp_idx_distribution_set_type_01", columnList = "tenant,deleted"), + @Index(name = "sp_idx_distribution_set_type_prim", columnList = "tenant,id") }, uniqueConstraints = { + @UniqueConstraint(columnNames = { "name", "tenant" }, name = "uk_dst_name"), + @UniqueConstraint(columnNames = { "type_key", "tenant" }, name = "uk_dst_key") }) +public class DistributionSetType extends NamedEntity { + + /** + * + */ + private static final long serialVersionUID = 1L; + + @OneToMany(targetEntity = DistributionSetTypeElement.class, cascade = { + CascadeType.ALL }, fetch = FetchType.EAGER, orphanRemoval = true) + @JoinColumn(name = "distribution_set_type", insertable = false, updatable = false) + private final Set elements = new HashSet<>(); + + @Column(name = "type_key", nullable = false, length = 64) + private String key; + + @Column(name = "colour", nullable = true, length = 16) + private String colour; + + @Column(name = "deleted") + private boolean deleted = false; + + public DistributionSetType() { + } + + /** + * Standard constructor. + * + * @param key + * of the type (unique) + * @param name + * of the type (unique) + * @param description + * of the type + */ + public DistributionSetType(final String key, final String name, final String description) { + this(key, name, description, null); + } + + /** + * Constructor. + * + * @param key + * of the type + * @param name + * of the type + * @param description + * of the type + * @param color + * of the type. It will be null by default + */ + public DistributionSetType(final String key, final String name, final String description, final String color) { + super(name, description); + this.key = key; + this.colour = color; + } + + /** + * @return the deleted + */ + public boolean isDeleted() { + return deleted; + } + + /** + * @param deleted + * the deleted to set + */ + public void setDeleted(final boolean deleted) { + this.deleted = deleted; + } + + public Set getMandatoryModuleTypes() { + return elements.stream().filter(element -> element.isMandatory()).map(element -> element.getSmType()) + .collect(Collectors.toSet()); + } + + public Set getOptionalModuleTypes() { + return elements.stream().filter(element -> !element.isMandatory()).map(element -> element.getSmType()) + .collect(Collectors.toSet()); + } + + /** + * Checks if the given {@link SoftwareModuleType} is in this + * {@link DistributionSetType}. + * + * @param softwareModuleType + * search for + * @return true if found + */ + public boolean containsModuleType(final SoftwareModuleType softwareModuleType) { + for (final DistributionSetTypeElement distributionSetTypeElement : elements) { + if (distributionSetTypeElement.getSmType().equals(softwareModuleType)) { + return true; + } + + } + return false; + } + + /** + * Checks if the given {@link SoftwareModuleType} is in this + * {@link DistributionSetType} and defined as + * {@link DistributionSetTypeElement#isMandatory()}. + * + * @param softwareModuleType + * search for + * @return true if found + */ + public boolean containsMandatoryModuleType(final SoftwareModuleType softwareModuleType) { + return elements.stream().filter(element -> element.isMandatory()) + .filter(element -> element.getSmType().equals(softwareModuleType)).findFirst().isPresent(); + + } + + /** + * Checks if the given {@link SoftwareModuleType} is in this + * {@link DistributionSetType} and NOT defined as + * {@link DistributionSetTypeElement#isMandatory()}. + * + * @param softwareModuleType + * search for + * @return true if found + */ + public boolean containsOptionalModuleType(final SoftwareModuleType softwareModuleType) { + return elements.stream().filter(element -> !element.isMandatory()) + .filter(element -> element.getSmType().equals(softwareModuleType)).findFirst().isPresent(); + + } + + /** + * Compares the modules of this {@link DistributionSetType} and the given + * one. + * + * @param dsType + * to compare with + * @return true if the lists are identical. + */ + public boolean areModuleEntriesIdentical(final DistributionSetType dsType) { + return new HashSet(dsType.elements).equals(elements); + } + + /** + * Adds {@link SoftwareModuleType} that is optional for the + * {@link DistributionSet}. + * + * @param smType + * to add + * @return updated instance + */ + public DistributionSetType addOptionalModuleType(final SoftwareModuleType smType) { + elements.add(new DistributionSetTypeElement(this, smType, false)); + + return this; + } + + /** + * Adds {@link SoftwareModuleType} that is mandatory for the + * {@link DistributionSet}. + * + * @param smType + * to add + * @return updated instance + */ + public DistributionSetType addMandatoryModuleType(final SoftwareModuleType smType) { + elements.add(new DistributionSetTypeElement(this, smType, true)); + + return this; + } + + /** + * Removes {@link SoftwareModuleType} from the list. + * + * @param smTypeId + * to remove + * @return updated instance + */ + public DistributionSetType removeModuleType(final Long smTypeId) { + // we search by id (standard equals compares also revison) + final Optional found = elements.stream() + .filter(element -> element.getSmType().getId().equals(smTypeId)).findFirst(); + + if (found.isPresent()) { + elements.remove(found.get()); + } + + return this; + } + + /** + * @return the key + */ + public String getKey() { + return key; + } + + /** + * @param key + * the key to set + */ + public void setKey(final String key) { + this.key = key; + } + + /** + * @param distributionSet + * to check for completeness + * @return true if the all mandatory software module types are + * in the system. + */ + public boolean checkComplete(final DistributionSet distributionSet) { + return distributionSet.getModules().stream().map(module -> module.getType()).collect(Collectors.toList()) + .containsAll(getMandatoryModuleTypes()); + } + + /** + * + * @return the DistributionSet type color + */ + public String getColour() { + return colour; + } + + /** + * + * @param colour + * the col + */ + public void setColour(final String colour) { + this.colour = colour; + } + + public Set getElements() { + return elements; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "DistributionSetType [key=" + key + ", isDeleted()=" + isDeleted() + ", getId()=" + getId() + "]"; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetTypeElement.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetTypeElement.java new file mode 100755 index 000000000..0d892f6da --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetTypeElement.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.io.Serializable; + +import javax.persistence.Column; +import javax.persistence.ConstraintMode; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MapsId; +import javax.persistence.Table; + +/** + * Relation element between a {@link DistributionSetType} and its + * {@link SoftwareModuleType} elements. + * + * + * + * + */ +@Entity +@Table(name = "sp_ds_type_element") +public class DistributionSetTypeElement implements Serializable { + + /** + * + */ + private static final long serialVersionUID = 1L; + + @EmbeddedId + private DistributionSetTypeElementCompositeKey key; + + @Column(name = "mandatory") + private boolean mandatory; + + @MapsId("dsType") + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "distribution_set_type", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_ds_type_element_dstype") ) + private DistributionSetType dsType; + + @MapsId("smType") + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "software_module_type", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_ds_type_element_smtype") ) + private SoftwareModuleType smType; + + /** + * Default constructor. + */ + public DistributionSetTypeElement() { + } + + /** + * Standard constructor. + * + * @param dsType + * of the element + * @param smType + * of the element + * @param mandatory + * to true if the {@link SoftwareModuleType} if + * mandatory element in the {@link DistributionSet}. + */ + public DistributionSetTypeElement(final DistributionSetType dsType, final SoftwareModuleType smType, + final boolean mandatory) { + super(); + this.key = new DistributionSetTypeElementCompositeKey(dsType, smType); + this.dsType = dsType; + this.smType = smType; + this.mandatory = mandatory; + } + + /** + * @return the mandatory + */ + public boolean isMandatory() { + return mandatory; + } + + /** + * @return the dsType + */ + public DistributionSetType getDsType() { + return dsType; + } + + /** + * @return the smType + */ + public SoftwareModuleType getSmType() { + return smType; + } + + /** + * @return the key + */ + public DistributionSetTypeElementCompositeKey getKey() { + return key; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetTypeElementCompositeKey.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetTypeElementCompositeKey.java new file mode 100755 index 000000000..c819d4e52 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DistributionSetTypeElementCompositeKey.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.io.Serializable; + +import javax.persistence.Column; +import javax.persistence.Embeddable; + +/** + * Composite key for {@link DistributionSetTypeElement}. + * + * + * + * + */ +@Embeddable +public class DistributionSetTypeElementCompositeKey implements Serializable { + + /** + * + */ + private static final long serialVersionUID = 1L; + + @Column(name = "distribution_set_type", nullable = false) + private Long dsType; + + @Column(name = "software_module_type", nullable = false) + private Long smType; + + /** + * Default constructor. + */ + DistributionSetTypeElementCompositeKey() { + } + + /** + * Constructor. + * + * @param dsType + * in the key + * @param smType + * in the key + */ + DistributionSetTypeElementCompositeKey(final DistributionSetType dsType, final SoftwareModuleType smType) { + super(); + this.dsType = dsType.getId(); + this.smType = smType.getId(); + } + + /** + * @return the dsType + */ + public Long getDsType() { + return dsType; + } + + /** + * @param dsType + * the dsType to set + */ + public void setDsType(final Long dsType) { + this.dsType = dsType; + } + + /** + * @return the smType + */ + public Long getSmType() { + return smType; + } + + /** + * @param smType + * the smType to set + */ + public void setSmType(final Long smType) { + this.smType = smType; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DsMetadataCompositeKey.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DsMetadataCompositeKey.java new file mode 100755 index 000000000..f26a66ab9 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/DsMetadataCompositeKey.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.io.Serializable; + +/** + * The DistributionSet Metadata composite key which contains the meta data key + * and the ID of the DistributionSet itself. + * + * + * + */ +public final class DsMetadataCompositeKey implements Serializable { + /** + * + */ + private static final long serialVersionUID = 1L; + + private String key; + + private Long distributionSet; + + /** + * + */ + public DsMetadataCompositeKey() { + } + + /** + * @param distributionSet + * the distribution set for this meta data + * @param key + * the key of the meta data + */ + public DsMetadataCompositeKey(final DistributionSet distributionSet, final String key) { + this.distributionSet = distributionSet.getId(); + this.key = key; + } + + /** + * @return the key + */ + public String getKey() { + return key; + } + + /** + * @param key + * the key to set + */ + public void setKey(final String key) { + this.key = key; + } + + /** + * @return the distributionSet + */ + public Long getDistributionSet() { + return distributionSet; + } + + /** + * @param distributionSet + * the distributionSet to set + */ + public void setDistributionSet(final Long distributionSet) { + this.distributionSet = distributionSet; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((distributionSet == null) ? 0 : distributionSet.hashCode()); + result = prime * result + ((key == null) ? 0 : key.hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { // NOSONAR - as this is generated + // code + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final DsMetadataCompositeKey other = (DsMetadataCompositeKey) obj; + if (distributionSet == null) { + if (other.distributionSet != null) { + return false; + } + } else if (!distributionSet.equals(other.distributionSet)) { + return false; + } + if (key == null) { + if (other.key != null) { + return false; + } + } else if (!key.equals(other.key)) { + return false; + } + return true; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/ExternalArtifact.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/ExternalArtifact.java new file mode 100755 index 000000000..d6664ac59 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/ExternalArtifact.java @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.net.URL; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.ConstraintMode; +import javax.persistence.Entity; +import javax.persistence.ForeignKey; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.validation.constraints.NotNull; + +/** + * External artifact representation with all the necessray informattion to + * generate an artifact {@link URL} at runtime. + * + * + * + * + */ +@Table(name = "sp_external_artifact", indexes = { + @Index(name = "sp_idx_external_artifact_prim", columnList = "id,tenant") }) +@Entity +public class ExternalArtifact extends Artifact { + private static final long serialVersionUID = 1L; + + @ManyToOne + @JoinColumn(name = "provider", nullable = false, updatable = false, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_art_to_ext_provider") ) + private ExternalArtifactProvider externalArtifactProvider; + + @Column(name = "url_suffix", length = 512) + private String urlSuffix; + + // CascadeType.PERSIST as we register ourself at the BSM + @ManyToOne(optional = false, cascade = { CascadeType.PERSIST }) + @JoinColumn(name = "software_module", nullable = false, updatable = false, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_external_assigned_sm") ) + private SoftwareModule softwareModule; + + /** + * Default constructor. + */ + public ExternalArtifact() { + super(); + } + + /** + * Constructs {@link ExternalArtifact}. + * + * @param externalArtifactProvider + * of the artifact + * @param urlSuffix + * of the artifact + * @param softwareModule + * of the artifact + */ + public ExternalArtifact(@NotNull final ExternalArtifactProvider externalArtifactProvider, final String urlSuffix, + final SoftwareModule softwareModule) { + setSoftwareModule(softwareModule); + this.externalArtifactProvider = externalArtifactProvider; + + if (urlSuffix != null) { + this.urlSuffix = urlSuffix; + } else { + this.urlSuffix = externalArtifactProvider.getDefaultSuffix(); + } + } + + /** + * @return the softwareModule + */ + @Override + public SoftwareModule getSoftwareModule() { + return softwareModule; + } + + /** + * @param softwareModule + * the softwareModule to set + */ + public final void setSoftwareModule(final SoftwareModule softwareModule) { + this.softwareModule = softwareModule; + this.softwareModule.addArtifact(this); + } + + /** + * @return the externalArtifactProvider + */ + public ExternalArtifactProvider getExternalArtifactProvider() { + return externalArtifactProvider; + } + + public String getUrl() { + return new StringBuilder().append(externalArtifactProvider.getBasePath()).append(urlSuffix).toString(); + } + + /** + * @return the urlSuffix + */ + public String getUrlSuffix() { + return urlSuffix; + } + + /** + * @param externalArtifactProvider + * the externalArtifactProvider to set + */ + public void setExternalArtifactProvider(final ExternalArtifactProvider externalArtifactProvider) { + this.externalArtifactProvider = externalArtifactProvider; + } + + /** + * @param urlSuffix + * the urlSuffix to set + */ + public void setUrlSuffix(final String urlSuffix) { + this.urlSuffix = urlSuffix; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + this.getClass().getName().hashCode(); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof ExternalArtifact)) { + return false; + } + + return true; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/ExternalArtifactProvider.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/ExternalArtifactProvider.java new file mode 100755 index 000000000..f6ed40e79 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/ExternalArtifactProvider.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; + +/** + * External repositories for artifact storage. The SP server provides URLs for + * the targets to download rom these external ressources but does not access + * thenm itself. + * + * + * + * + */ +@Table(name = "sp_external_provider", indexes = { + @Index(name = "sp_idx_external_provider_prim", columnList = "tenant,id") }) +@Entity +public class ExternalArtifactProvider extends NamedEntity { + private static final long serialVersionUID = 1L; + + @Column(name = "base_url", length = 512, nullable = false) + private String basePath; + + @Column(name = "default_url_suffix", length = 512, nullable = true) + private String defaultSuffix; + + /** + * Constructs {@link ExternalArtifactProvider} based on given properties. + * + * @param name + * of the provided + * @param description + * which is optional + * @param baseURL + * of all {@link ExternalArtifact}s of the provider + * @param defaultUrlSuffix + * that is used if {@link ExternalArtifact#getUrlSuffix()} is + * empty. + */ + public ExternalArtifactProvider(final String name, final String description, final String baseURL, + final String defaultUrlSuffix) { + super(name, description); + basePath = baseURL; + defaultSuffix = defaultUrlSuffix; + } + + ExternalArtifactProvider() { + super(); + defaultSuffix = ""; + basePath = ""; + } + + /** + * @return the basePath + */ + public String getBasePath() { + return basePath; + } + + /** + * @return the defaultSuffix + */ + public String getDefaultSuffix() { + return defaultSuffix; + } + + /** + * @param basePath + * the basePath to set + */ + public void setBasePath(final String basePath) { + this.basePath = basePath; + } + + /** + * @param defaultSuffix + * the defaultSuffix to set + */ + public void setDefaultSuffix(final String defaultSuffix) { + this.defaultSuffix = defaultSuffix; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + this.getClass().getName().hashCode(); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof ExternalArtifactProvider)) { + return false; + } + + return true; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/LocalArtifact.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/LocalArtifact.java new file mode 100755 index 000000000..a5ad493b9 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/LocalArtifact.java @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.ConstraintMode; +import javax.persistence.Entity; +import javax.persistence.ForeignKey; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.validation.constraints.NotNull; + +import com.mongodb.gridfs.GridFS; +import com.mongodb.gridfs.GridFSFile; + +/** + * Tenant specific locally stored artifact representation that is used by + * {@link SoftwareModule} . It contains all information that is provided by the + * user while all SP server generated information related to the artifact (hash, + * length) is stored directly with the binary itself. + * + * + * + */ +@Table(name = "sp_artifact", indexes = { @Index(name = "sp_idx_artifact_01", columnList = "tenant,software_module"), + @Index(name = "sp_idx_artifact_prim", columnList = "tenant,id") }) +@Entity +public class LocalArtifact extends Artifact { + private static final long serialVersionUID = 1L; + + @NotNull + @Column(name = "gridfs_file_name", length = 40) + private String gridFsFileName; + + @NotNull + @Column(name = "provided_file_name", length = 256) + private String filename; + + @ManyToOne(optional = false, cascade = { CascadeType.PERSIST }) + @JoinColumn(name = "software_module", nullable = false, updatable = false, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_assigned_sm") ) + private SoftwareModule softwareModule; + + /** + * Default constructor. + */ + public LocalArtifact() { + super(); + } + + /** + * Constructs artifact. + * + * @param gridFsFileName + * that is the link to the {@link GridFS} entity. + * @param filename + * that is used by {@link GridFSFile} store. + * @param softwareModule + * of this artifact + */ + public LocalArtifact(@NotNull final String gridFsFileName, @NotNull final String filename, + final SoftwareModule softwareModule) { + setSoftwareModule(softwareModule); + this.gridFsFileName = gridFsFileName; + this.filename = filename; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + this.getClass().getName().hashCode(); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof LocalArtifact)) { + return false; + } + + return true; + } + + /** + * @return the softwareModule + */ + @Override + public SoftwareModule getSoftwareModule() { + return softwareModule; + } + + /** + * @param softwareModule + * the softwareModule to set + */ + public final void setSoftwareModule(final SoftwareModule softwareModule) { + this.softwareModule = softwareModule; + this.softwareModule.addArtifact(this); + } + + /** + * @return the gridFsFileName + */ + public String getGridFsFileName() { + return gridFsFileName; + } + + /** + * @return the filename + */ + public String getFilename() { + return filename; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/NamedEntity.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/NamedEntity.java new file mode 100755 index 000000000..0cebd0362 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/NamedEntity.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; + +/** + * {@link BaseEntity} extension for all entities that are named in addition to + * their technical ID. + * + * + * + * + * + * + */ +@MappedSuperclass +public abstract class NamedEntity extends BaseEntity { + private static final long serialVersionUID = 1L; + + @Column(name = "name", nullable = false, length = 64) + private String name; + + @Column(name = "description", nullable = true, length = 512) + private String description; + + /** + * Default constructor. + */ + public NamedEntity() { + super(); + } + + /** + * Parameterized constructor. + * + * @param name + * of the {@link NamedEntity} + * @param description + * of the {@link NamedEntity} + */ + public NamedEntity(final String name, final String description) { + this.name = name; + this.description = description; + } + + public String getDescription() { + return description; + } + + public String getName() { + return name; + } + + public void setDescription(final String description) { + this.description = description; + } + + public void setName(final String name) { + this.name = name; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/NamedVersionedEntity.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/NamedVersionedEntity.java new file mode 100755 index 000000000..77819ed46 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/NamedVersionedEntity.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; + +/** + * Extension for {@link NamedEntity} that are versioned. + * + * + * + * + * + * + */ +@MappedSuperclass +public abstract class NamedVersionedEntity extends NamedEntity { + private static final long serialVersionUID = 1L; + + @Column(name = "version", nullable = false, length = 64) + private String version; + + /** + * parameterized constructor. + * + * @param name + * of the entity + * @param version + * of the entity + * @param description + */ + public NamedVersionedEntity(final String name, final String version, final String description) { + super(name, description); + this.version = version; + } + + NamedVersionedEntity() { + super(); + } + + public String getVersion() { + return version; + } + + public void setVersion(final String version) { + this.version = version; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModule.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModule.java new file mode 100755 index 000000000..a0c510010 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModule.java @@ -0,0 +1,293 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.ConstraintMode; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; +import javax.persistence.NamedAttributeNode; +import javax.persistence.NamedEntityGraph; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +import org.eclipse.persistence.annotations.CascadeOnDelete; + +/** + * Base Software Module that is supported by OS level provisioning mechanism on + * the edge controller, e.g. OS, JVM, AgentHub + * + * + * + * + * + * + */ +@Entity +@Table(name = "sp_base_software_module", uniqueConstraints = @UniqueConstraint(columnNames = { "module_type", "name", + "version", "tenant" }, name = "uk_base_sw_mod") , indexes = { + @Index(name = "sp_idx_base_sw_module_01", columnList = "tenant,deleted,name,version"), + @Index(name = "sp_idx_base_sw_module_02", columnList = "tenant,deleted,module_type"), + @Index(name = "sp_idx_base_sw_module_prim", columnList = "tenant,id") }) +@NamedEntityGraph(name = "SoftwareModule.artifacts", attributeNodes = { @NamedAttributeNode("artifacts") }) +public class SoftwareModule extends NamedVersionedEntity { + private static final long serialVersionUID = 1L; + + @ManyToOne + @JoinColumn(name = "module_type", nullable = false, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_module_type") ) + private SoftwareModuleType type; + + @ManyToMany(mappedBy = "modules", targetEntity = DistributionSet.class, fetch = FetchType.LAZY) + private final List assignedTo = new ArrayList(); + + @Column(name = "deleted") + private boolean deleted = false; + + @Column(name = "vendor", nullable = true, length = 256) + private String vendor; + + @OneToMany(mappedBy = "softwareModule", cascade = { CascadeType.ALL }, targetEntity = LocalArtifact.class) + private List artifacts; + + @OneToMany(mappedBy = "softwareModule", cascade = { CascadeType.ALL }, targetEntity = ExternalArtifact.class) + private List externalArtifacts; + + @CascadeOnDelete + @OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.REMOVE }) + @JoinColumn(name = "sw_id", insertable = false, updatable = false) + private final List metadata = new ArrayList<>(); + + /** + * Default constructor. + */ + public SoftwareModule() { + super(); + } + + /** + * parameterized constructor. + * + * @param type + * of the {@link SoftwareModule} + * @param name + * abstract name of the {@link SoftwareModule} + * @param version + * of the {@link SoftwareModule} + * @param description + * of the {@link SoftwareModule} + * @param vendor + * of the {@link SoftwareModule} + */ + public SoftwareModule(final SoftwareModuleType type, final String name, final String version, + final String description, final String vendor) { + super(name, version, description); + this.vendor = vendor; + this.type = type; + } + + /** + * @param artifact + * is added to the assigned {@link Artifact}s. + */ + public void addArtifact(final LocalArtifact artifact) { + if (null == artifacts) { + artifacts = new ArrayList<>(4); + } + + if (!artifacts.contains(artifact)) { + artifacts.add(artifact); + } + } + + /** + * @param artifact + * is added to the assigned {@link Artifact}s. + */ + public void addArtifact(final ExternalArtifact artifact) { + if (null == externalArtifacts) { + externalArtifacts = new ArrayList<>(4); + } + + if (!externalArtifacts.contains(artifact)) { + externalArtifacts.add(artifact); + } + + } + + /** + * @param artifactId + * to look for + * @return found {@link Artifact} + */ + public Optional getLocalArtifact(final Long artifactId) { + if (null == artifacts) { + return Optional.empty(); + } + + return artifacts.stream().filter(artifact -> artifact.getId().equals(artifactId)).findFirst(); + } + + /** + * @param fileName + * to look for + * @return found {@link Artifact} + */ + public Optional getLocalArtifactByFilename(final String fileName) { + if (null == artifacts) { + return Optional.empty(); + } + + return artifacts.stream().filter(artifact -> artifact.getFilename().equalsIgnoreCase(fileName.trim())) + .findFirst(); + } + + /** + * @return the artifacts + */ + public List getArtifacts() { + final List result = new ArrayList(); + result.addAll(artifacts); + result.addAll(externalArtifacts); + + return result; + } + + /** + * @return local artifacts only + */ + public List getLocalArtifacts() { + if (artifacts == null) { + return Collections.emptyList(); + } + + return artifacts; + } + + public String getVendor() { + return vendor; + } + + /** + * @param artifact + * is removed from the assigned {@link LocalArtifact}s. + */ + public void removeArtifact(final LocalArtifact artifact) { + if (null != artifacts) { + artifacts.remove(artifact); + } + } + + /** + * @param artifact + * is removed from the assigned {@link ExternalArtifact}s. + */ + public void removeArtifact(final ExternalArtifact artifact) { + if (null != externalArtifacts) { + externalArtifacts.remove(artifact); + } + } + + public void setVendor(final String vendor) { + this.vendor = vendor; + } + + public SoftwareModuleType getType() { + return type; + } + + /** + * @return the deleted + */ + public boolean isDeleted() { + return deleted; + } + + /** + * @param deleted + * the deleted to set + */ + public void setDeleted(final boolean deleted) { + this.deleted = deleted; + } + + public void setType(final SoftwareModuleType type) { + this.type = type; + } + + /** + * @return the metadata + */ + public List getMetadata() { + return metadata; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "SoftwareModule [type=" + type + ", deleted=" + deleted + ", getVersion()=" + getVersion() + + ", getOptLockRevision()=" + getOptLockRevision() + ", getId()=" + getId() + "]"; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + this.getClass().getName().hashCode(); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof SoftwareModule)) { + return false; + } + + return true; + } + + /** + * @return the assignedTo + */ + public List getAssignedTo() { + return assignedTo; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModuleIdName.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModuleIdName.java new file mode 100755 index 000000000..8658de3c8 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModuleIdName.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.io.Serializable; + +/** + * To hold software module name and Id. + * + * + * + */ +public class SoftwareModuleIdName implements Serializable { + + private static final long serialVersionUID = -6317413180936148514L; + + private final Long id; + private final String name; + + /** + * @param id + * if the {@link SoftwareModule} + * @param name + * of the {@link SoftwareModule} + */ + public SoftwareModuleIdName(final Long id, final String name) { + super(); + this.id = id; + this.name = name; + } + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() {// NOSONAR - as this is generated + final int prime = 31; + int result = 1; + result = prime * result + (id == null ? 0 : id.hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) {// NOSONAR - as this is generated + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final SoftwareModuleIdName other = (SoftwareModuleIdName) obj; + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + return true; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModuleMetadata.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModuleMetadata.java new file mode 100755 index 000000000..d4ddba3f3 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModuleMetadata.java @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.io.Serializable; + +import javax.persistence.Column; +import javax.persistence.ConstraintMode; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +/** + * Metadata for {@link SoftwareModule}. + * + * + * + * + */ +@IdClass(SwMetadataCompositeKey.class) +@Entity +@Table(name = "sp_sw_metadata") +public class SoftwareModuleMetadata implements Serializable { + + /** + * + */ + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "meta_key", length = 128) + private String key; + + @Column(name = "meta_value", length = 4000) + private String value; + + @Id + @ManyToOne(targetEntity = SoftwareModule.class, fetch = FetchType.LAZY) + @JoinColumn(name = "sw_id", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_metadata_sw") ) + private SoftwareModule softwareModule; + + public SoftwareModuleMetadata() { + + } + + /** + * Standard constructor. + * + * @param key + * of the metadata element + * @param softwareModule + * @param value + * of the metadata element + */ + public SoftwareModuleMetadata(final String key, final SoftwareModule softwareModule, final String value) { + this.key = key; + this.softwareModule = softwareModule; + this.value = value; + } + + /** + * @return the id + */ + public SwMetadataCompositeKey getId() { + return new SwMetadataCompositeKey(softwareModule, key); + } + + /** + * @return the value + */ + public String getValue() { + return value; + } + + /** + * @param value + * the value to set + */ + public void setValue(final String value) { + this.value = value; + } + + /** + * @return the softwareModule + */ + public SoftwareModule getSoftwareModule() { + return softwareModule; + } + + /** + * @param softwareModule + * the softwareModule to set + */ + public void setSoftwareModule(final SoftwareModule softwareModule) { + this.softwareModule = softwareModule; + } + + /** + * @return the key + */ + public String getKey() { + return key; + } + + /** + * @param key + * the key to set + */ + public void setKey(final String key) { + this.key = key; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModuleType.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModuleType.java new file mode 100755 index 000000000..0810d408e --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModuleType.java @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * Type of a software modules. + * + * + * + * + */ +@Entity +@Table(name = "sp_software_module_type", indexes = { + @Index(name = "sp_idx_software_module_type_01", columnList = "tenant,deleted"), + @Index(name = "sp_idx_software_module_type_prim", columnList = "tenant,id") }, uniqueConstraints = { + @UniqueConstraint(columnNames = { "type_key", "tenant" }, name = "uk_smt_type_key"), + @UniqueConstraint(columnNames = { "name", "tenant" }, name = "uk_smt_name") }) +public class SoftwareModuleType extends NamedEntity { + /** + * + */ + private static final long serialVersionUID = 1L; + + @Column(name = "type_key", nullable = false, length = 64) + private String key; + + @Column(name = "max_ds_assignments", nullable = false) + private int maxAssignments; + + @Column(name = "colour", nullable = true, length = 16) + private String colour; + + @Column(name = "deleted") + private boolean deleted = false; + + /** + * Constructor. + * + * @param key + * of the type + * @param name + * of the type + * @param description + * of the type + * @param maxAssignments + * assignments to a DS + */ + public SoftwareModuleType(final String key, final String name, final String description, final int maxAssignments) { + this(key, name, description, maxAssignments, null); + } + + /** + * Constructor. + * + * @param key + * of the type + * @param name + * of the type + * @param description + * of the type + * @param maxAssignments + * assignments to a DS + * @param colour + * of the type. It will be null by default + */ + public SoftwareModuleType(final String key, final String name, final String description, final int maxAssignments, + final String colour) { + super(); + this.key = key; + this.maxAssignments = maxAssignments; + setDescription(description); + setName(name); + this.colour = colour; + } + + /** + * Default Constructor. + */ + public SoftwareModuleType() { + super(); + } + + /** + * @return the key + */ + public String getKey() { + return key; + } + + /** + * @return the max + */ + public int getMaxAssignments() { + return maxAssignments; + } + + /** + * @return the deleted + */ + public boolean isDeleted() { + return deleted; + } + + /** + * @param deleted + * the deleted to set + */ + public void setDeleted(final boolean deleted) { + this.deleted = deleted; + } + + /** + * + * @return the software type color + */ + public String getColour() { + return colour; + } + + /** + * + * @param colour + * the col + */ + public void setColour(final String colour) { + this.colour = colour; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/SwMetadataCompositeKey.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/SwMetadataCompositeKey.java new file mode 100755 index 000000000..2518e4b1b --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/SwMetadataCompositeKey.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.io.Serializable; + +/** + * The Software Module Metadata composite key which contains the meta data key + * and the ID of the software module itself. + * + * + * + */ +public final class SwMetadataCompositeKey implements Serializable { + /** + * + */ + private static final long serialVersionUID = 1L; + + private String key; + + private Long softwareModule; + + /** + * + */ + public SwMetadataCompositeKey() { + } + + /** + * @param softwareModule + * the software module for this meta data + * @param key + * the key of the meta data + */ + public SwMetadataCompositeKey(final SoftwareModule softwareModule, final String key) { + this.softwareModule = softwareModule.getId(); + this.key = key; + } + + /** + * @return the key + */ + public String getKey() { + return key; + } + + /** + * @param key + * the key to set + */ + public void setKey(final String key) { + this.key = key; + } + + /** + * @return the softwareModule + */ + public Long getSoftwareModule() { + return softwareModule; + } + + /** + * @param softwareModule + * the softwareModule to set + */ + public void setSoftwareModule(final Long softwareModule) { + this.softwareModule = softwareModule; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((softwareModule == null) ? 0 : softwareModule.hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { // NOSONAR - as this is generated + // code + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final SwMetadataCompositeKey other = (SwMetadataCompositeKey) obj; + if (key == null) { + if (other.key != null) { + return false; + } + } else if (!key.equals(other.key)) { + return false; + } + if (softwareModule == null) { + if (other.softwareModule != null) { + return false; + } + } else if (!softwareModule.equals(other.softwareModule)) { + return false; + } + return true; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/Tag.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/Tag.java new file mode 100755 index 000000000..c03a02f3e --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/Tag.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; + +import org.springframework.hateoas.Identifiable; + +/** + * A Tag can be used as describing and organisational meta information for any + * kind of entity. + * + * + * + * + * + * + */ +@MappedSuperclass +public abstract class Tag extends NamedEntity implements Identifiable { + private static final long serialVersionUID = 1L; + + @Column(name = "colour", nullable = true, length = 16) + private String colour; + + protected Tag() { + super(); + } + + /** + * Public constructor. + * + * @param name + * of the {@link Tag} + * @param description + * of the {@link Tag} + * @param colour + * of tag in UI + */ + public Tag(final String name, final String description, final String colour) { + super(name, description); + this.colour = colour; + } + + public String getColour() { + return colour; + } + + public void setColour(final String colour) { + this.colour = colour; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Tag [getOptLockRevision()=" + getOptLockRevision() + ", getId()=" + getId() + "]"; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/Target.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/Target.java new file mode 100755 index 000000000..6f37b2d8b --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/Target.java @@ -0,0 +1,265 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.ConstraintMode; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; +import javax.persistence.NamedAttributeNode; +import javax.persistence.NamedEntityGraph; +import javax.persistence.OneToMany; +import javax.persistence.OneToOne; +import javax.persistence.PrimaryKeyJoinColumn; +import javax.persistence.Table; +import javax.persistence.Transient; +import javax.persistence.UniqueConstraint; + +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.repository.model.helper.SecurityChecker; +import org.eclipse.hawkbit.repository.model.helper.SecurityTokenGeneratorHolder; +import org.eclipse.persistence.annotations.CascadeOnDelete; +import org.springframework.data.domain.Persistable; + +/** + *

+ * The {@link Target} is the target of all provisioning operations. It contains + * the currently installed {@link DistributionSet} (i.e. current state). In + * addition it holds the target {@link DistributionSet} that has to be + * provisioned next (i.e. target state). + *

+ * + *

+ * {@link #getStatus()}s() shows if the {@link Target} is + * {@link TargetStatus#IN_SYNC} or a provisioning is + * {@link TargetStatus#PENDING} or the target is only + * {@link TargetStatus#REGISTERED}, i.e. a target {@link DistributionSet} . + *

+ * + * + * + * + * + * + */ +@Entity +@Table(name = "sp_target", indexes = { + @Index(name = "sp_idx_target_01", columnList = "tenant,name,assigned_distribution_set"), + @Index(name = "sp_idx_target_02", columnList = "tenant,name"), + @Index(name = "sp_idx_target_03", columnList = "tenant,controller_id,assigned_distribution_set"), + @Index(name = "sp_idx_target_04", columnList = "tenant,created_at"), + @Index(name = "sp_idx_target_prim", columnList = "tenant,id") }, uniqueConstraints = @UniqueConstraint(columnNames = { + "controller_id", "tenant" }, name = "uk_tenant_controller_id") ) +@NamedEntityGraph(name = "Target.detail", attributeNodes = { @NamedAttributeNode("tags"), + @NamedAttributeNode(value = "assignedDistributionSet"), @NamedAttributeNode(value = "targetInfo") }) +public class Target extends NamedEntity implements Persistable { + private static final long serialVersionUID = 1L; + + @Column(name = "controller_id", length = 64) + private String controllerId; + + @Transient + private boolean isNew = false; + + @ManyToMany(targetEntity = TargetTag.class) + @JoinTable(name = "sp_target_target_tag", joinColumns = { + @JoinColumn(name = "target", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_targ_targtag_target") ) }, inverseJoinColumns = { + @JoinColumn(name = "tag", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_targ_targtag_tag") ) }) + private Set tags = new HashSet(); + + @CascadeOnDelete + @OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, cascade = { CascadeType.REMOVE }) + @JoinColumn(name = "target", insertable = false, updatable = false, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_targ_act_hist_targ") ) + private final List actions = new ArrayList<>(); + + @ManyToOne(optional = true, fetch = FetchType.LAZY, targetEntity = DistributionSet.class) + @JoinColumn(name = "assigned_distribution_set", nullable = true, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_target_assign_ds") ) + private DistributionSet assignedDistributionSet; + + @CascadeOnDelete + @OneToOne(cascade = { CascadeType.ALL }, fetch = FetchType.LAZY, targetEntity = TargetInfo.class) + @PrimaryKeyJoinColumn + private TargetInfo targetInfo = null; + + /** + * the security token of the target which allows if enabled to authenticate + * with this security token. + */ + @Column(name = "sec_token", insertable = true, updatable = true, nullable = false, length = 128) + private String securityToken = null; + + /** + * Constructor. + * + * @param controllerId + * controller ID of the {@link Target} + */ + public Target(final String controllerId) { + this.controllerId = controllerId; + setName(controllerId); + securityToken = SecurityTokenGeneratorHolder.getInstance().generateToken(); + targetInfo = new TargetInfo(this); + } + + /** + * empty constructor for JPA. + */ + Target() { + controllerId = null; + securityToken = null; + } + + @Override + public boolean equals(final Object obj) {// NOSONAR - as this is generated + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Target other = (Target) obj; + if (controllerId == null) { + if (other.controllerId != null) { + return false; + } + } else if (!controllerId.equals(other.controllerId)) { + return false; + } + return true; + } + + public DistributionSet getAssignedDistributionSet() { + return assignedDistributionSet; + } + + public String getControllerId() { + return controllerId; + } + + public Set getTags() { + return tags; + } + + @Override + public int hashCode() { // NOSONAR - as this is generated + final int prime = 31; + int result = 1; + result = prime * result + (controllerId == null ? 0 : controllerId.hashCode()); + return result; + } + + public void setAssignedDistributionSet(final DistributionSet assignedDistributionSet) { + this.assignedDistributionSet = assignedDistributionSet; + } + + /** + * @param controllerId + * the controllerId to set + */ + public void setControllerId(final String controllerId) { + this.controllerId = controllerId; + } + + /** + * @param tags + * the tags to set + */ + public void setTags(final Set tags) { + this.tags = tags; + } + + /** + * @return the actions + */ + public List getActions() { + return actions; + } + + public TargetIdName getTargetIdName() { + return new TargetIdName(getId(), getControllerId(), getName()); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Persistable#isNew() + */ + @Override + @Transient + public boolean isNew() { + return isNew; + } + + /** + * @param isNew + * the isNew to set + */ + public void setNew(final boolean isNew) { + this.isNew = isNew; + } + + /** + * @return the targetInfo + */ + public TargetInfo getTargetInfo() { + return targetInfo; + } + + /** + * @param targetInfo + * the targetInfo to set + */ + public void setTargetInfo(final TargetInfo targetInfo) { + this.targetInfo = targetInfo; + } + + /** + * @return the securityToken + */ + public String getSecurityToken() { + if (SecurityChecker.hasPermission(SpPermission.READ_TARGET_SEC_TOKEN)) { + return securityToken; + } + return null; + } + + /** + * @param securityToken + * the securityToken to set + */ + public void setSecurityToken(final String securityToken) { + this.securityToken = securityToken; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Target [controllerId=" + controllerId + ", getId()=" + getId() + "]"; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TargetFilterQuery.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TargetFilterQuery.java new file mode 100755 index 000000000..8d76b7ae5 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TargetFilterQuery.java @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * + * + */ +@Entity +@Table(name = "sp_target_filter_query", indexes = { + @Index(name = "sp_idx_target_filter_query_01", columnList = "tenant,name") }, uniqueConstraints = @UniqueConstraint(columnNames = { + "name", "tenant" }, name = "uk_tenant_custom_filter_name") ) +public class TargetFilterQuery extends BaseEntity { + /** + * + * + */ + private static final long serialVersionUID = 7493966984413479089L; + + @Column(name = "name", length = 64) + private String name; + + @Column(name = "query", length = 1024) + private String query; + + public TargetFilterQuery() { + name = null; + query = null; + } + + public TargetFilterQuery(final String name, final String query) { + this.name = name; + this.query = query; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name + * the name to set + */ + public void setName(final String name) { + this.name = name; + } + + /** + * @return the query + */ + public String getQuery() { + return query; + } + + /** + * @param query + * the query to set + */ + public void setQuery(final String query) { + this.query = query; + } + + @Override + public boolean equals(final Object obj) {// NOSONAR - as this is generated + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final TargetFilterQuery other = (TargetFilterQuery) obj; + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + return true; + } + + @Override + public int hashCode() { // NOSONAR - as this is generated + final int prime = 31; + int result = 1; + result = prime * result + (name == null ? 0 : name.hashCode()); + return result; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TargetIdName.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TargetIdName.java new file mode 100755 index 000000000..f7a63b775 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TargetIdName.java @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.io.Serializable; + +/** + * ID class of the {@link Target} which contains the + * {@link Target#getControllerId()} and the {@link Target#getName()} in one + * object. Often it's necessary to remeber the IDs of the {@link Target} and the + * resolve for e.g. the UI the name of the target, this is very costly + * operation, so it's much better if the ID and the name of the {@link Target} + * is already in memory available. + * + * + * + * + */ +public class TargetIdName implements Serializable { + /** + * + */ + private static final long serialVersionUID = 1L; + + private final long targetId; + private String controllerId; + private String name; + + /** + * @param targetId + * the ID of the target. + * @param controllerId + * the {@link Target#getControllerId()} + * @param name + * the {@link Target#getName()} + */ + public TargetIdName(final long targetId, final String controllerId, final String name) { + this.targetId = targetId; + this.controllerId = controllerId; + this.name = name; + } + + /** + * @return the controller id + */ + public String getControllerId() { + return controllerId; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param id + * the id to set + */ + public void setControllerId(final String id) { + this.controllerId = id; + } + + /** + * @param name + * the name to set + */ + public void setName(final String name) { + this.name = name; + } + + /** + * @return the targetId + */ + public long getTargetId() { + return targetId; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (int) (targetId ^ (targetId >>> 32)); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final TargetIdName other = (TargetIdName) obj; + if (targetId != other.targetId) { + return false; + } + return true; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + // only return the ID because it's used in vaadin for setting the item + // id in the dom + return controllerId; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TargetInfo.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TargetInfo.java new file mode 100755 index 000000000..d491319fa --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TargetInfo.java @@ -0,0 +1,400 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.io.Serializable; +import java.net.URI; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.persistence.CascadeType; +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ConstraintMode; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MapKeyColumn; +import javax.persistence.MapsId; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import javax.persistence.Transient; + +import org.eclipse.hawkbit.repository.model.helper.PollConfigurationHelper; +import org.eclipse.persistence.annotations.CascadeOnDelete; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Persistable; + +/** + * A table which contains all the information inserted, updated by the + * controller itself. So this entity does not provide audit information because + * changes on this entity are mostly only done by controller requests. That's + * the reason that we store these information in a separated table so we don't + * modifying the {@link Target} itself when a controller reports it's + * {@link #lastTargetQuery} for example. + * + * + * + * + */ +@Table(name = "sp_target_info", indexes = { + @Index(name = "sp_idx_target_info_02", columnList = "target_id,update_status") }) +@Entity +// @DynamicUpdate +public class TargetInfo implements Persistable, Serializable { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private static final Logger LOG = LoggerFactory.getLogger(TargetInfo.class); + + @Id + private Long targetId; + + @Transient + private boolean isNew = false; + + @CascadeOnDelete + @OneToOne(cascade = { CascadeType.MERGE, CascadeType.REMOVE }, fetch = FetchType.LAZY, targetEntity = Target.class) + @JoinColumn(name = "target_id", nullable = false, updatable = false, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_targ_stat_targ") ) + @MapsId + // use deprecated annotation until HHH-8862 is fixed + // @SuppressWarnings( "deprecation" ) + // @org.hibernate.annotations.ForeignKey( name = "fk_targ_stat_targ" ) + private Target target; + + @Column(name = "address", length = 512) + private String address = null; + + @Column(name = "last_target_query") + private Long lastTargetQuery = null; + + @Column(name = "install_date") + private Long installationDate; + + @Column(name = "update_status", nullable = false, length = 255) + @Enumerated(EnumType.STRING) + private TargetUpdateStatus updateStatus = TargetUpdateStatus.UNKNOWN; + + @ManyToOne(optional = true, fetch = FetchType.LAZY) + @JoinColumn(name = "installed_distribution_set", nullable = true, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_target_inst_ds") ) + private DistributionSet installedDistributionSet; + + /** + * Read only on management API. Are commited by controller. + */ + @ElementCollection + @Column(name = "attribute_value", length = 128) + @MapKeyColumn(name = "attribute_key", nullable = false, length = 32) + @CollectionTable(name = "sp_target_attributes", joinColumns = { + @JoinColumn(name = "target_id") }, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_targ_attrib_target") ) + // use deprecated annotation until HHH-8862 is fixed + @SuppressWarnings("deprecation") + // @org.hibernate.annotations.ForeignKey( name = "fk_targ_attrib_target" ) + private final Map controllerAttributes = Collections.synchronizedMap(new HashMap()); + + // set default request controller attributes to true, because we want to + // request them the first + // time + @Column(name = "request_controller_attributes", nullable = false) + private boolean requestControllerAttributes = true; + + /** + * Constructor for {@link TargetStatus}. + * + * @param target + * related to this status. + */ + public TargetInfo(final Target target) { + this.target = target; + targetId = target.getId(); + } + + TargetInfo() { + target = null; + targetId = null; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Persistable#getId() + */ + @Override + public Long getId() { + return targetId; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Persistable#isNew() + */ + @Override + @Transient + public boolean isNew() { + return isNew; + } + + /** + * @param isNew + * the isNew to set + */ + public void setNew(final boolean isNew) { + this.isNew = isNew; + } + + /** + * @return the ipAddress + */ + public URI getAddress() { + if (address == null) { + return null; + } + try { + return URI.create(address); + } catch (final IllegalArgumentException e) { + LOG.warn("Invalid address provided. Cloud not be configured to URI", e); + return null; + } + } + + /** + * @param ipAddress + * the ipAddress to set + * + * @throws IllegalArgumentException + * If the given string violates RFC 2396 + */ + public void setAddress(final String address) { + // check if this is a real URI + if (address != null) { + URI.create(address); + } + + this.address = address; + } + + /** + * @return the targetId + */ + public Long getTargetId() { + return targetId; + } + + /** + * @param targetId + * the targetId to set + */ + public void setTargetId(final Long targetId) { + this.targetId = targetId; + } + + /** + * @return the target + */ + public Target getTarget() { + return target; + } + + /** + * @param target + * the target to set + */ + public void setTarget(final Target target) { + this.target = target; + } + + /** + * @return the lastTargetQuery + */ + public Long getLastTargetQuery() { + return lastTargetQuery; + } + + /** + * @param lastTargetQuery + * the lastTargetQuery to set + */ + public void setLastTargetQuery(final Long lastTargetQuery) { + this.lastTargetQuery = lastTargetQuery; + } + + /** + * @param requestControllerAttributes + * the requestControllerAttributes to set + */ + public void setRequestControllerAttributes(final boolean requestControllerAttributes) { + this.requestControllerAttributes = requestControllerAttributes; + } + + /** + * @return the controllerAttributes + */ + public Map getControllerAttributes() { + return controllerAttributes; + } + + /** + * @return the requestControllerAttributes + */ + public boolean isRequestControllerAttributes() { + return requestControllerAttributes; + } + + /** + * @return the installationDate + */ + public Long getInstallationDate() { + return installationDate; + } + + /** + * @param installationDate + * the installationDate to set + */ + public void setInstallationDate(final Long installationDate) { + this.installationDate = installationDate; + } + + /** + * @return the updateStatus + */ + public TargetUpdateStatus getUpdateStatus() { + return updateStatus; + } + + /** + * @param updateStatus + * the updateStatus to set + */ + public void setUpdateStatus(final TargetUpdateStatus updateStatus) { + this.updateStatus = updateStatus; + } + + /** + * @return the installedDistributionSet + */ + public DistributionSet getInstalledDistributionSet() { + return installedDistributionSet; + } + + /** + * @param installedDistributionSet + * the installedDistributionSet to set + */ + public void setInstalledDistributionSet(final DistributionSet installedDistributionSet) { + this.installedDistributionSet = installedDistributionSet; + } + + /** + * @return the poll time which holds the last poll time of the target, the + * next poll time and the overdue time. In case the + * {@link #lastTargetQuery} is not set e.g. the target never polled + * before this method returns {@code null} + */ + public PollStatus getPollStatus() { + if (lastTargetQuery != null) { + final Duration pollTime = PollConfigurationHelper.getInstance().getPollTimeInterval(); + final Duration overdueTime = PollConfigurationHelper.getInstance().getOverduePollTimeInterval(); + final LocalDateTime currentDate = LocalDateTime.now(); + final LocalDateTime lastPollDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastTargetQuery), + ZoneId.systemDefault()); + final LocalDateTime nextPollDate = lastPollDate.plus(pollTime); + final LocalDateTime overdueDate = nextPollDate.plus(overdueTime); + return new PollStatus(lastPollDate, nextPollDate, overdueDate, currentDate); + } + return null; + } + + /** + * The poll time object which holds all the necessary information around the + * target poll time, e.g. the last poll time, the next poll time and the + * overdue poll time. + * + * + * + */ + public static final class PollStatus { + private final LocalDateTime lastPollDate; + private final LocalDateTime nextPollDate; + private final LocalDateTime overdueDate; + private final LocalDateTime currentDate; + + private PollStatus(final LocalDateTime lastPollDate, final LocalDateTime nextPollDate, + final LocalDateTime overdueDate, final LocalDateTime currentDate) { + this.lastPollDate = lastPollDate; + this.nextPollDate = nextPollDate; + this.overdueDate = overdueDate; + this.currentDate = currentDate; + } + + /** + * calculates if the target poll time is overdue and the target has not + * been polled in the configured poll time interval. + * + * @return {@code true} if the current time is after the poll time + * overdue date otherwise {@code false}. + */ + public boolean isOverdue() { + return currentDate.isAfter(overdueDate); + } + + /** + * @return the lastPollDate + */ + public LocalDateTime getLastPollDate() { + return lastPollDate; + } + + /** + * @return the nextPollDate + */ + public LocalDateTime getNextPollDate() { + return nextPollDate; + } + + /** + * @return the overdueDate + */ + public LocalDateTime getOverdueDate() { + return overdueDate; + } + + public LocalDateTime getCurrentDate() { + return currentDate; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "PollTime [lastPollDate=" + lastPollDate + ", nextPollDate=" + nextPollDate + ", overdueDate=" + + overdueDate + ", currentDate=" + currentDate + "]"; + } + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TargetTag.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TargetTag.java new file mode 100755 index 000000000..781bf9d16 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TargetTag.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.util.List; + +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Index; +import javax.persistence.ManyToMany; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * A {@link TargetTag} is used to describe Target attributes and use them also + * for filtering the target list. + * + * + * + * + * + * + */ +@Entity +@Table(name = "sp_target_tag", indexes = { + @Index(name = "sp_idx_target_tag_prim", columnList = "tenant,id") }, uniqueConstraints = @UniqueConstraint(columnNames = { + "name", "tenant" }, name = "uk_targ_tag") ) +public class TargetTag extends Tag { + private static final long serialVersionUID = 1L; + + @ManyToMany(mappedBy = "tags", targetEntity = Target.class, fetch = FetchType.LAZY) + private List assignedToTargets; + + /** + * Constructor. + * + * @param name + * of {@link TargetTag} + * @param description + * of {@link TargetTag} + * @param colour + * of {@link TargetTag} + */ + public TargetTag(final String name, final String description, final String colour) { + super(name, description, colour); + } + + /** + * Public constructor. + * + * @param name + * of the {@link TargetTag} + **/ + public TargetTag(final String name) { + super(name, null, null); + } + + TargetTag() { + super(); + } + + /** + * @return the assignedToTargets + */ + public List getAssignedToTargets() { + return assignedToTargets; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + this.getClass().getName().hashCode(); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof TargetTag)) { + return false; + } + + return true; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TargetUpdateStatus.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TargetUpdateStatus.java new file mode 100755 index 000000000..cecbf8339 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TargetUpdateStatus.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +/** + * The overall {@link TargetUpdateStatus} of a {@link Target} that describes its + * status. A {@link Target} can have only one status. independent of the number + * of {@link UpdateAction}s that have to be applied. + * + * + * + * + * + * + */ +public enum TargetUpdateStatus { + + /** + * Set by default to targets until first status update received from the + * target. + */ + UNKNOWN, + + /** + * Assigned {@link DistributionSet} is installed. + */ + IN_SYNC, + + /** + * Installation of assigned {@link DistributionSet} is no yet confirmed. + */ + PENDING, + + /** + * Installation of assigned {@link DistributionSet} has failed. + */ + ERROR, + + /** + * Controller registered at SP but no {@link DistributionSet} assigned. + */ + REGISTERED; +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TenantConfiguration.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TenantConfiguration.java new file mode 100755 index 000000000..d571a2ea7 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TenantConfiguration.java @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.io.Serializable; + +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * A JPA entity which stores the tenant specific configuration. + * + * + * + * + */ +@Entity +@Table(name = "sp_tenant_configuration", uniqueConstraints = @UniqueConstraint(columnNames = { "conf_key", + "tenant" }, name = "uk_tenant_key") ) +public class TenantConfiguration extends BaseEntity implements Serializable { + + /** + * + */ + private static final long serialVersionUID = 1L; + + @Column(name = "conf_key", length = 128) + private String key; + + @Column(name = "conf_value", length = 512) + @Basic + private String value; + + /** + * JPA default constructor. + */ + public TenantConfiguration() { + + } + + /** + * @param key + * the key of this configuration + * @param value + * the value of this configuration + */ + public TenantConfiguration(final String key, final String value) { + this.key = key; + this.value = value; + + } + + /** + * @return the key + */ + public String getKey() { + return key; + } + + /** + * @param key + * the key to set + */ + public void setKey(final String key) { + this.key = key; + } + + /** + * @return the value + */ + public String getValue() { + return value; + } + + /** + * @param value + * the value to set + */ + public void setValue(final String value) { + this.value = value; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { // NOSONAR - as this is generated + // code + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final TenantConfiguration other = (TenantConfiguration) obj; + if (key == null) { + if (other.key != null) { + return false; + } + } else if (!key.equals(other.key)) { + return false; + } + return true; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TenantMetaData.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TenantMetaData.java new file mode 100755 index 000000000..e86bfe8a4 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/TenantMetaData.java @@ -0,0 +1,259 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.io.Serializable; + +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Column; +import javax.persistence.ConstraintMode; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.persistence.Version; + +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; + +/** + * Tenant metadata that is configured globally for the entire tenant. This + * entity is + * + * + * + * + */ +@Access(AccessType.FIELD) +@Table(name = "sp_tenant", indexes = { + @Index(name = "sp_idx_tenant_prim", columnList = "tenant,id") }, uniqueConstraints = { + @UniqueConstraint(columnNames = { "tenant" }, name = "uk_tenantmd_tenant") }) +@Entity +public class TenantMetaData implements Serializable { + + /** + * + */ + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "tenant", nullable = false, length = 40) + private String tenant; + + private String createdBy; + private String lastModifiedBy; + private Long createdAt; + private Long lastModifiedAt; + + @Version + @Column(name = "optlock_revision") + private long optLockRevision; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "default_ds_type", nullable = false, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_tenant_md_default_ds_type") ) + // use deprecated annotation until HHH-8862 is fixed + @SuppressWarnings("deprecation") + // @org.hibernate.annotations.ForeignKey( name = + // "fk_tenant_md_default_ds_type" ) + private DistributionSetType defaultDsType; + + public TenantMetaData() { + } + + /** + * Standard constructor. + * + * @param defaultDsType + * of this tenant + * @param tenant + */ + public TenantMetaData(final DistributionSetType defaultDsType, final String tenant) { + super(); + this.defaultDsType = defaultDsType; + this.tenant = tenant; + } + + /** + * @return the defaultDsType + */ + public DistributionSetType getDefaultDsType() { + return defaultDsType; + } + + /** + * Set the DistributionSet for a tenant. + * + * @param defaultDsType + * the new default DistributionSet + */ + public void setDefaultDsType(final DistributionSetType defaultDsType) { + this.defaultDsType = defaultDsType; + } + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id + * the id to set + */ + public void setId(final Long id) { + this.id = id; + } + + @Access(AccessType.PROPERTY) + @Column(name = "created_at", insertable = true, updatable = false) + public Long getCreatedAt() { + return createdAt; + } + + @Access(AccessType.PROPERTY) + @Column(name = "created_by", insertable = true, updatable = false, length = 40) + public String getCreatedBy() { + return createdBy; + } + + @Access(AccessType.PROPERTY) + @Column(name = "last_modified_at", insertable = false, updatable = true) + public Long getLastModifiedAt() { + return lastModifiedAt; + } + + @Access(AccessType.PROPERTY) + @Column(name = "last_modified_by", insertable = false, updatable = true, length = 40) + public String getLastModifiedBy() { + return lastModifiedBy; + } + + /** + * @param createdBy + * the createdBy to set + */ + @CreatedBy + public void setCreatedBy(final String createdBy) { + this.createdBy = createdBy; + } + + /** + * @param lastModifiedBy + * the lastModifiedBy to set + */ + @LastModifiedBy + public void setLastModifiedBy(final String lastModifiedBy) { + this.lastModifiedBy = lastModifiedBy; + } + + /** + * @param createdAt + * the createdAt to set + */ + @CreatedDate + public void setCreatedAt(final Long createdAt) { + this.createdAt = createdAt; + } + + /** + * @param lastModifiedAt + * the lastModifiedAt to set + */ + @LastModifiedDate + public void setLastModifiedAt(final Long lastModifiedAt) { + this.lastModifiedAt = lastModifiedAt; + } + + /** + * @return the optLockRevision + */ + public long getOptLockRevision() { + return optLockRevision; + } + + /** + * @param optLockRevision + * the optLockRevision to set + */ + public void setOptLockRevision(final long optLockRevision) { + this.optLockRevision = optLockRevision; + } + + /** + * @return the tenant + */ + public String getTenant() { + return tenant; + } + + /** + * @param tenant + * the tenant to set + */ + public void setTenant(final String tenant) { + this.tenant = tenant; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { // NOSONAR - as this is generated + // code + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final TenantMetaData other = (TenantMetaData) obj; + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + return true; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/CacheManagerHolder.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/CacheManagerHolder.java new file mode 100755 index 000000000..c8c081d95 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/CacheManagerHolder.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model.helper; + +import org.eclipse.hawkbit.eventbus.CacheFieldEntityListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; + +/** + * A singleton bean which holds the {@link CacheManager} to have to the cache + * manager in beans not instantiated by spring e.g. JPA entities or + * {@link CacheFieldEntityListener} which cannot be autowired. + * + * + * + */ +public final class CacheManagerHolder { + + private static final CacheManagerHolder SINGLETON = new CacheManagerHolder(); + + @Autowired + private CacheManager cacheManager; + + private CacheManagerHolder() { + + } + + /** + * @return the cache manager holder singleton instance + */ + public static CacheManagerHolder getInstance() { + return SINGLETON; + } + + /** + * @return the cacheManager + */ + public CacheManager getCacheManager() { + return cacheManager; + } + + /** + * Normally not used when using spring-boot then the cachemanager is + * autowired to the CacheManagerHolder, but for testing purposes. + * + * @param cacheManager + * the cache manager to set for the cache manager holder. + */ + public void setCacheManager(final CacheManager cacheManager) { + this.cacheManager = cacheManager; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/PollConfigurationHelper.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/PollConfigurationHelper.java new file mode 100755 index 000000000..7f5978a2d --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/PollConfigurationHelper.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model.helper; + +import java.time.Duration; + +import javax.annotation.PostConstruct; + +import org.eclipse.hawkbit.ControllerPollProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.EnvironmentAware; + +/** + * A singleton bean which holds configuration of the poll time + * {@code hawkbit.server.controller.polling} and + * {@code hawkbit.server.controller.polling.overdue} to have access to the + * configuration in beans not instatinated by spring e.g. JPA entities which + * cannot implement the {@link EnvironmentAware} interface to retrieve + * environment variables. + * + * + * + * + */ +public final class PollConfigurationHelper { + + private static final Logger LOG = LoggerFactory.getLogger(PollConfigurationHelper.class); + private static final PollConfigurationHelper INSTANCE = new PollConfigurationHelper(); + + private static final int DEFAULT_OVERDUE_HOUR = 0; + private static final int DEFAULT_OVERDUE_MINUTE = 5; + private static final int DEFAULT_OVERDUE_SECOND = 0; + + private static final int DEFAULT_POLL_HOUR = 0; + private static final int DEFAULT_POLL_MINUTE = 5; + private static final int DEFAULT_POLL_SECOND = 0; + + @Autowired + private ControllerPollProperties controllerPollProperties; + + private Duration controllerPollTimeDuration; + private Duration controllerOverduePollTimeDuration; + + private PollConfigurationHelper() { + } + + /** + * Bean post construct to calcualte the poll time and poll overdue time only + * once. + */ + @PostConstruct + public void postConstruct() { + final long[] controllerPollTimeSplit = splitInterval(controllerPollProperties.getPollingTime(), + DEFAULT_POLL_HOUR, DEFAULT_POLL_MINUTE, DEFAULT_POLL_SECOND); + this.controllerPollTimeDuration = Duration.ZERO.plusHours(controllerPollTimeSplit[0]) + .plusMinutes(controllerPollTimeSplit[1]).plusSeconds(controllerPollTimeSplit[2]); + + final long[] controllerOverduePollTimeSplit = splitInterval(controllerPollProperties.getPollingOverdueTime(), + DEFAULT_OVERDUE_HOUR, DEFAULT_OVERDUE_MINUTE, DEFAULT_OVERDUE_SECOND); + this.controllerOverduePollTimeDuration = Duration.ZERO.plusHours(controllerOverduePollTimeSplit[0]) + .plusMinutes(controllerOverduePollTimeSplit[1]).plusSeconds(controllerOverduePollTimeSplit[2]); + + } + + /** + * @return the poll time interval configured in the configuration + * {@code hawkbit.server.controller.polling} or the default value + * which is {@code 00:05:00} never {@code null}. + */ + public Duration getPollTimeInterval() { + return controllerPollTimeDuration; + } + + /** + * @return the overdue poll threshold configured in the configuration + * {@code hawkbit.server.controller.polling.overdue} or the default + * value which is {@code 00:05:00} never {@code null}. + */ + public Duration getOverduePollTimeInterval() { + return controllerOverduePollTimeDuration; + } + + /** + * @return a singleton instance of the environment helper. + */ + public static PollConfigurationHelper getInstance() { + return INSTANCE; + } + + private long[] splitInterval(final String interval, final long defaultHour, final long defaultMinute, + final long defaultSecond) { + if (interval != null) { + final String[] split = interval.split(":"); + if (split.length == 3) { + try { + return new long[] { Long.parseLong(split[0]), Long.parseLong(split[1]), Long.parseLong(split[2]) }; + } catch (final NumberFormatException e) { + LOG.warn("Cannot parse the given poll configuration {}", interval); + } + } + } + LOG.warn("Using default configuration hour:{} min:{}, second:{}", defaultHour, defaultMinute, defaultSecond); + return new long[] { defaultHour, defaultMinute, defaultSecond }; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/SecurityChecker.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/SecurityChecker.java new file mode 100755 index 000000000..7c9763e6d --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/SecurityChecker.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model.helper; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * A helper class which allows to do runtime security checks. + * + * + * + * + */ +public final class SecurityChecker { + + private SecurityChecker() { + + } + + /** + * Checks the current {@link SecurityContext} for the permission. + * + * @param permission + * the permission to check against the security context + * @return {@code true} if the given permission is present in the security + * context otherwise {@code false} + */ + public static boolean hasPermission(final String permission) { + final SecurityContext context = SecurityContextHolder.getContext(); + if (context != null) { + final Authentication authentication = context.getAuthentication(); + if (authentication != null) { + for (final GrantedAuthority authority : authentication.getAuthorities()) { + if (authority.getAuthority().equals(permission)) { + return true; + } + } + } + } + return false; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/SecurityTokenGeneratorHolder.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/SecurityTokenGeneratorHolder.java new file mode 100755 index 000000000..ffc1e308f --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/SecurityTokenGeneratorHolder.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model.helper; + +import org.eclipse.hawkbit.security.SecurityTokenGenerator; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * A singleton bean which holds the {@link SecurityTokenGenerator} and make it + * accessible to beans which are not managed by spring, e.g. JPA entities. + * + * + * + * + */ +public final class SecurityTokenGeneratorHolder { + + private static final SecurityTokenGeneratorHolder INSTANCE = new SecurityTokenGeneratorHolder(); + + @Autowired + private SecurityTokenGenerator securityTokenGenerator; + + /** + * private constructor. + */ + private SecurityTokenGeneratorHolder() { + + } + + /** + * @return a singleton instance of the security token generator holder. + */ + public static SecurityTokenGeneratorHolder getInstance() { + return INSTANCE; + } + + /** + * delegates to {@link SecurityTokenGenerator#generateToken()}. + * + * @return the result {@link SecurityTokenGenerator#generateToken()} + */ + public String generateToken() { + return securityTokenGenerator.generateToken(); + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/SystemManagementHolder.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/SystemManagementHolder.java new file mode 100755 index 000000000..d1327c690 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/SystemManagementHolder.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model.helper; + +import org.eclipse.hawkbit.repository.SystemManagement; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * A singleton bean which holds {@link SystemManagement} service and makes it + * accessible to beans which are not managed by spring, e.g. JPA entities. + * + * + * + **/ +public final class SystemManagementHolder { + + private static final SystemManagementHolder INSTANCE = new SystemManagementHolder(); + + @Autowired + private SystemManagement systemManagement; + + private SystemManagementHolder() { + } + + /** + * @return the singleton {@link SystemManagementHolder} instance + */ + public static SystemManagementHolder getInstance() { + return INSTANCE; + } + + /** + * @return the systemManagement + */ + public SystemManagement getSystemManagement() { + return systemManagement; + } + + /** + * @param systemManagement + * the systemManagement to set + */ + public void setSystemManagement(final SystemManagement systemManagement) { + this.systemManagement = systemManagement; + } + + /** + * @return the {@link SystemManagement#currentTenant()}. + */ + public String currentTenant() { + return systemManagement.currentTenant(); + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/TenantAwareHolder.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/TenantAwareHolder.java new file mode 100755 index 000000000..5b85745d7 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/model/helper/TenantAwareHolder.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model.helper; + +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * A singleton bean which holds {@link TenantAware} service and makes it + * accessible to beans which are not managed by spring, e.g. JPA entities. + * + * + * + * + */ +public final class TenantAwareHolder { + + private static final TenantAwareHolder INSTANCE = new TenantAwareHolder(); + + @Autowired + private TenantAware tenantAware; + + private TenantAwareHolder() { + } + + /** + * @return the singleton {@link TenantAwareHolder} instance + */ + public static TenantAwareHolder getInstance() { + return INSTANCE; + } + + /** + * @return the {@link TenantAware} service + */ + public TenantAware getTenantAware() { + return tenantAware; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/rsql/PropertyMapper.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/rsql/PropertyMapper.java new file mode 100755 index 000000000..ffa0e9212 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/rsql/PropertyMapper.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.rsql; + +import java.util.HashMap; +import java.util.Map; + +/** + * Maps property of entity to its allias . + * + * + * + */ +public final class PropertyMapper { + + private static Map, Map> allowedColmns = new HashMap<>(); + + private PropertyMapper() { + + } + + /** + * Add new mapping - property name and alias. + * + * @param type + * entity type + * @param property + * alias of property + * @param mapping + * property name + */ + public static void addNewMapping(final Class type, final String property, final String mapping) { + if (allowedColmns.get(type) == null) { + allowedColmns.put(type, new HashMap()); + } + allowedColmns.get(type).put(property, mapping); + } + + /** + * @return the allowedcolmns + */ + public static Map, Map> getAllowedcolmns() { + return allowedColmns; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/rsql/RSQLParameterSyntaxException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/rsql/RSQLParameterSyntaxException.java new file mode 100755 index 000000000..324de8eba --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/rsql/RSQLParameterSyntaxException.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.rsql; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * Exception used by the REST API in case of RSQL search filter query. + * + * + * + * + */ +public class RSQLParameterSyntaxException extends SpServerRtException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new RSQLSyntaxException with + * {@link SpServerError#SP_REST_RSQL_SEARCH_PARAM_SYNTAX} error. + */ + public RSQLParameterSyntaxException() { + super(SpServerError.SP_REST_RSQL_SEARCH_PARAM_SYNTAX); + } + + /** + * Creates a new RSQLSyntaxException with + * {@link SpServerError#SP_REST_RSQL_SEARCH_PARAM_SYNTAX} error. + * + * @param cause + * the cause of this exception + */ + public RSQLParameterSyntaxException(final Throwable cause) { + super(SpServerError.SP_REST_RSQL_SEARCH_PARAM_SYNTAX, cause); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/rsql/RSQLParameterUnsupportedFieldException.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/rsql/RSQLParameterUnsupportedFieldException.java new file mode 100755 index 000000000..e95368acb --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/rsql/RSQLParameterUnsupportedFieldException.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.rsql; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * Exception used by the REST API in case of invalid field name in the rsql + * search parameter. + * + * + * + * + */ +public class RSQLParameterUnsupportedFieldException extends SpServerRtException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new RSQLParameterUnsupportedFieldException with + * {@link SpServerError#SP_REST_RSQL_PARAM_INVALID_FIELD} error. + */ + public RSQLParameterUnsupportedFieldException() { + super(SpServerError.SP_REST_RSQL_PARAM_INVALID_FIELD); + } + + /** + * Creates a new RSQLParameterUnsupportedFieldException with + * {@link SpServerError#SP_REST_RSQL_PARAM_INVALID_FIELD} error. + * + * @param cause + * the cause (which is saved for later retrieval by the + * getCause() method). (A null value is permitted, and indicates + * that the cause is nonexistent or unknown.) + */ + public RSQLParameterUnsupportedFieldException(final Throwable cause) { + super(SpServerError.SP_REST_RSQL_PARAM_INVALID_FIELD, cause); + } + + /** + * Creates a new RSQLParameterUnsupportedFieldException with + * {@link SpServerError#SP_REST_RSQL_PARAM_INVALID_FIELD} error. + * + * @param message + * the message of the exception + * @param cause + * the cause (which is saved for later retrieval by the + * getCause() method). (A null value is permitted, and indicates + * that the cause is nonexistent or unknown.) + */ + public RSQLParameterUnsupportedFieldException(final String message, final Throwable cause) { + super(message, SpServerError.SP_REST_RSQL_PARAM_INVALID_FIELD, cause); + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/rsql/RSQLUtility.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/rsql/RSQLUtility.java new file mode 100755 index 000000000..2fcbaac22 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/rsql/RSQLUtility.java @@ -0,0 +1,467 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.rsql; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.persistence.EntityManager; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Path; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import javax.persistence.metamodel.Attribute.PersistentAttributeType; +import javax.persistence.metamodel.ManagedType; +import javax.persistence.metamodel.Metamodel; +import javax.persistence.metamodel.PluralAttribute; + +import org.eclipse.hawkbit.repository.FieldNameProvider; +import org.eclipse.hawkbit.repository.FieldValueConverter; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.Target; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.jpa.domain.Specification; + +import cz.jirutka.rsql.parser.RSQLParser; +import cz.jirutka.rsql.parser.RSQLParserException; +import cz.jirutka.rsql.parser.ast.AndNode; +import cz.jirutka.rsql.parser.ast.ComparisonNode; +import cz.jirutka.rsql.parser.ast.ComparisonOperator; +import cz.jirutka.rsql.parser.ast.LogicalNode; +import cz.jirutka.rsql.parser.ast.Node; +import cz.jirutka.rsql.parser.ast.OrNode; +import cz.jirutka.rsql.parser.ast.RSQLOperators; +import cz.jirutka.rsql.parser.ast.RSQLVisitor; + +/** + * A utility class which is able to parse RSQL strings into an spring data + * {@link Specification} which then can be enhanced sql queries to filter + * entities. RSQL parser library: https://github.com/jirutka/rsql-parser + * + *
    + *
  • Equal to : ==
  • + *
  • Not equal to : !=
  • + *
  • Less than : =lt= or <
  • + *
  • Less than or equal to : =le= or <=
  • + *
  • Greater than operator : =gt= or >
  • + *
  • Greater than or equal to : =ge= or >=
  • + *
+ * Examples of RSQL expressions in both FIQL-like and alternative notation: + *
    + *
  • version==2.0.0
  • + *
  • name==targetId1;description==plugAndPlay
  • + *
  • name==targetId1 and description==plugAndPlay
  • + *
  • name==targetId1;description==plugAndPlay
  • + *
  • name==targetId1 and description==plugAndPlay
  • + *
  • name==targetId1,description==plugAndPlay,updateStatus==UNKNOWN
  • + *
  • name==targetId1 or description==plugAndPlay or updateStatus==UNKNOWN
  • + *
+ * + * + * + * + */ +public final class RSQLUtility { + + private static final Logger LOGGER = LoggerFactory.getLogger(RSQLUtility.class); + + /** + * private constructor due utility class. + */ + private RSQLUtility() { + + } + + /** + * parses an RSQL valid string into an JPA {@link Specification} which then + * can be used to filter for JPA entities with the given RSQL query. + * + * @param rsql + * the rsql query + * @param fieldNameProvider + * the enum class type which implements the + * {@link FieldNameProvider} + * @param entityManager + * {@link EntityManager} + * @return an specification which can be used with JPA + * @throws RSQLParameterUnsupportedFieldException + * if a field in the RSQL string is used but not provided by the + * given {@code fieldNameProvider} + * @throws RSQLParameterSyntaxException + * if the RSQL syntax is wrong + */ + public static & FieldNameProvider, T> Specification parse(final String rsql, + final Class fieldNameProvider, final EntityManager entityManager) { + return new RSQLSpecification<>(rsql, fieldNameProvider, entityManager); + } + + private static final class RSQLSpecification & FieldNameProvider, T> implements Specification { + + private final String rsql; + private final Class enumType; + private final EntityManager entityManager; + + private RSQLSpecification(final String rsql, final Class enumType, final EntityManager entityManager) { + this.rsql = rsql; + this.enumType = enumType; + this.entityManager = entityManager; + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.domain.Specification#toPredicate(javax. + * persistence.criteria .Root, javax.persistence.criteria.CriteriaQuery, + * javax.persistence.criteria.CriteriaBuilder) + */ + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { + + final Node rootNode; + try { + LOGGER.debug("parsing rsql string {}", rsql); + final Set operators = RSQLOperators.defaultOperators(); + operators.add(new ComparisonOperator("=li=", false)); + rootNode = new RSQLParser(operators).parse(rsql); + } catch (final RSQLParserException e) { + throw new RSQLParameterSyntaxException(e); + } + + final JpqQueryRSQLVisitor jpqQueryRSQLVisitor = new JpqQueryRSQLVisitor<>(root, cb, enumType, + entityManager); + final List accept = rootNode., String> accept(jpqQueryRSQLVisitor); + + if (accept != null && !accept.isEmpty()) { + return cb.and(accept.toArray(new Predicate[accept.size()])); + } + return cb.conjunction(); + + } + } + + /** + * An implementation of the {@link RSQLVisitor} to visit the parsed tokens + * and build jpa where clauses. + * + * + * + * @param + * the enum for providing the field name of the entity field to + * filter on. + * @param + * the entity type referenced by the root + */ + private static final class JpqQueryRSQLVisitor & FieldNameProvider, T> implements + RSQLVisitor, String> { + public static final Character LIKE_WILDCARD = '*'; + + static { + /** + * Property mapping are done in FieldNameProvider like + * TargetFields,SoftwareModuleFields etc. + * + * In addition to this mapping in PropertyMapper are done if we want + * to drill down on entity . + * + * For example : Drill down on distribution set of target entity are + * done by adding the mappings in PropertyMapper as below. + * + * User can now use assignedds.name and assignedds.version + * + */ + PropertyMapper.addNewMapping(Target.class, "assignedds", "assignedDistributionSet"); + PropertyMapper.addNewMapping(DistributionSet.class, "name", "name"); + PropertyMapper.addNewMapping(DistributionSet.class, "version", "version"); + } + + private final Root root; + private final CriteriaBuilder cb; + private final Class enumType; + private final EntityManager entityManager; + + private JpqQueryRSQLVisitor(final Root root, final CriteriaBuilder cb, final Class enumType, + final EntityManager entityManager) { + this.root = root; + this.cb = cb; + this.enumType = enumType; + this.entityManager = entityManager; + } + + @Override + public List visit(final AndNode node, final String param) { + final List childs = acceptChilds(node); + if (!childs.isEmpty()) { + return toSingleList(cb.and(childs.toArray(new Predicate[childs.size()]))); + } + return toSingleList(cb.conjunction()); + } + + @Override + public List visit(final OrNode node, final String param) { + final List childs = acceptChilds(node); + if (!childs.isEmpty()) { + return toSingleList(cb.or(childs.toArray(new Predicate[childs.size()]))); + } + return toSingleList(cb.conjunction()); + } + + private static boolean isItAssociationType(final String property, final ManagedType classMetadata) { + return classMetadata.getAttribute(property).isAssociation(); + } + + private static Class getPropertyType(final String property, final ManagedType classMetadata) { + Class propertyType; + if (classMetadata.getAttribute(property).isCollection()) { + propertyType = ((PluralAttribute) classMetadata.getAttribute(property)).getBindableJavaType(); + } else { + propertyType = classMetadata.getAttribute(property).getJavaType(); + } + return propertyType; + } + + private static ManagedType getClassMetaData(final String property, final ManagedType classMetadata, + final Metamodel metaModel) { + if (isItAssociationType(property, classMetadata)) { + final Class associationType = getPropertyType(property, classMetadata); + return metaModel.managedType(associationType); + } else { + if (isItEmbeddedType(property, classMetadata)) { + final Class embeddedType = getPropertyType(property, classMetadata); + return metaModel.managedType(embeddedType); + } + } + return classMetadata; + } + + private static boolean isItEmbeddedType(final String property, final ManagedType classMetadata) { + return classMetadata.getAttribute(property).getPersistentAttributeType() == PersistentAttributeType.EMBEDDED; + } + + private String validatePropertyFieldName(final String propertyFieldName, final boolean isDefinedInEnum, + ManagedType classMetadata, final Metamodel metaModel, final ComparisonNode node) { + String finalProperty = propertyFieldName; + final String[] graph = propertyFieldName.split("\\."); + for (String property : graph) { + if (!isDefinedInEnum && PropertyMapper.getAllowedcolmns().containsKey(classMetadata.getJavaType())) { + if (PropertyMapper.getAllowedcolmns().get(classMetadata.getJavaType()).get(property) != null) { + final String mappedValue = PropertyMapper.getAllowedcolmns().get(classMetadata.getJavaType()) + .get(property); + finalProperty = finalProperty.replace(property, mappedValue); + property = mappedValue; + } else { + throw new RSQLParameterUnsupportedFieldException("The given search parameter field {" + + node.getSelector() + "} does not exist, must be one of the following fields {" + + getExpectedFieldList() + "}", new Exception()); + } + } + classMetadata = getClassMetaData(property, classMetadata, metaModel); + } + return finalProperty; + } + + private Path getFieldPath(final String finalProperty) { + Path fieldPath = null; + final String[] split = finalProperty.split("\\."); + if (split.length == 0) { + fieldPath = root.get(split[0]); + } else { + for (final String fieldNameSplit : split) { + // hibernate workaround because cannot get attribute of an + // PluralAttributePath, needs + // an implicit join. + // https://hibernate.atlassian.net/browse/HHH-7892 + if (fieldPath == null && root.get(fieldNameSplit) != null + && Collection.class.isAssignableFrom(root.get(fieldNameSplit).getJavaType())) { + fieldPath = root.join(fieldNameSplit); + } else { + fieldPath = (fieldPath != null) ? fieldPath.get(fieldNameSplit) : root.get(fieldNameSplit); + } + } + } + return fieldPath; + } + + @Override + public List visit(final ComparisonNode node, final String param) { + final Metamodel metaModel = entityManager.getMetamodel(); + final ManagedType classMetadata = metaModel.managedType(root.getJavaType()); + String propertyFieldName = null; + Boolean isDefinedInEnum = Boolean.FALSE; + A fieldName = null; + try { + /** + * Get the property mapping from FieldNameProvider .If not + * available check in PropertyMapping.If not found throw + * RSQLParameterUnsupportedFieldException. + */ + fieldName = getFieldIdentifierByName(node); + propertyFieldName = fieldName.getFieldName(); + isDefinedInEnum = Boolean.TRUE; + } catch (final IllegalArgumentException e) { + if (PropertyMapper.getAllowedcolmns().containsKey(classMetadata.getJavaType())) { + propertyFieldName = node.getSelector(); + } else { + throw new RSQLParameterUnsupportedFieldException("The given search parameter field {" + + node.getSelector() + + "} does not exist, must be one of the following fields {" + + Arrays.stream(enumType.getEnumConstants()).map(v -> v.name().toLowerCase()) + .collect(Collectors.toList()) + "}", e); + } + } + final String finalProperty = validatePropertyFieldName(propertyFieldName, isDefinedInEnum, classMetadata, + metaModel, node); + + final String value = node.getArguments().get(0); + Object transformedValue = convertValueIfNecessary(node, fieldName, value); + final Path fieldPath = getFieldPath(finalProperty); + // in case the value of an rsql query e.g. type==application is an + // enum we need to + // handle it separately because JPA needs the correct java-type to + // build an + // expression. So String and numeric values JPA can do it by it's + // own but not for + // classes like enums. So we need to transform the given value + // string into the enum + // class. + final Class javaType = fieldPath.getJavaType(); + if (javaType != null && javaType.isEnum()) { + transformedValue = transformEnumValue(node, value, javaType); + } + return mapToPredicate(node, fieldPath, value, transformedValue); + } + + private List getExpectedFieldList() { + final List expectedFieldList = Arrays.stream(enumType.getEnumConstants()) + .map(v -> v.name().toLowerCase()).collect(Collectors.toList()); + expectedFieldList.add("assignedds.name"); + expectedFieldList.add("assignedds.version"); + return expectedFieldList; + } + + private A getFieldIdentifierByName(final ComparisonNode node) { + final String enumName = node.getSelector().toUpperCase(); + LOGGER.debug("get fieldidentifier by name {} of enum type {}", enumName, enumType); + return Enum.valueOf(enumType, enumName); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Object convertValueIfNecessary(final ComparisonNode node, final A fieldName, final String value) { + if (fieldName instanceof FieldValueConverter) { + final Object convertedValue = ((FieldValueConverter) fieldName).convertValue(fieldName, value); + if (convertedValue == null) { + throw new RSQLParameterUnsupportedFieldException("field {" + node.getSelector() + + "} must be one of the following values {" + + Arrays.toString(((FieldValueConverter) fieldName).possibleValues(fieldName)) + "}", null); + } else { + return convertedValue; + } + } + return value; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Object transformEnumValue(final ComparisonNode node, final String value, + final Class javaType) { + final Class tmpEnumType = (Class) javaType; + try { + return Enum.valueOf(tmpEnumType, value.toUpperCase()); + } catch (final IllegalArgumentException e) { + // we could not transform the given string value into the enum + // type, so ignore + // it and return null and do not filter + LOGGER.info("given value {} cannot be transformed into the correct enum type {}", value.toUpperCase(), + javaType); + LOGGER.debug("value cannot be transformed to an enum", e); + + throw new RSQLParameterUnsupportedFieldException("field {" + + node.getSelector() + + "} must be one of the following values {" + + Arrays.stream(tmpEnumType.getEnumConstants()).map(v -> v.name().toLowerCase()) + .collect(Collectors.toList()) + "}", e); + } + } + + private List mapToPredicate(final ComparisonNode node, final Path fieldPath, + final String value, final Object transformedValue) { + // only 'equal' and 'notEqual' can handle transformed value like + // enums. The JPA API + // cannot handle object types for greaterThan etc methods. + final List singleList; + switch (node.getOperator().getSymbol()) { + case "=li=": + singleList = toSingleList(cb.like(cb.upper(pathOfString(fieldPath)), transformedValue.toString() + .toUpperCase())); + break; + case "==": + singleList = getEqualToPredicate(transformedValue, fieldPath); + break; + case "!=": + singleList = toSingleList(cb.notEqual(fieldPath, transformedValue)); + break; + case "=gt=": + singleList = toSingleList(cb.greaterThan(pathOfString(fieldPath), value)); + break; + case "=ge=": + singleList = toSingleList(cb.greaterThanOrEqualTo(pathOfString(fieldPath), value)); + break; + case "=lt=": + singleList = toSingleList(cb.lessThan(pathOfString(fieldPath), value)); + break; + case "=le=": + singleList = toSingleList(cb.lessThanOrEqualTo(pathOfString(fieldPath), value)); + break; + default: + LOGGER.info("operator symbol {} is either not supported or not implemented"); + singleList = Collections.emptyList(); + } + return singleList; + } + + private List getEqualToPredicate(final Object transformedValue, final Path fieldPath) { + if (transformedValue instanceof String) { + final String preFormattedValue = ((String) transformedValue).replace(LIKE_WILDCARD, '%'); + return toSingleList(cb.like(cb.upper(pathOfString(fieldPath)), preFormattedValue.toString() + .toUpperCase())); + } else { + return toSingleList(cb.equal(fieldPath, transformedValue)); + } + } + + @SuppressWarnings("unchecked") + private Path pathOfString(final Path path) { + return (Path) path; + } + + private List acceptChilds(final LogicalNode node) { + final List children = node.getChildren(); + final List childs = new ArrayList<>(); + for (final Node node2 : children) { + final List accept = node2.accept(this); + if (accept != null && !accept.isEmpty()) { + childs.addAll(accept); + } else { + LOGGER.debug("visit logical node children but could not parse it, ignoring {}", node2); + } + } + return childs; + } + + private static List toSingleList(final Predicate p) { + return Collections.singletonList(p); + } + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/specifications/DistributionSetSpecification.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/specifications/DistributionSetSpecification.java new file mode 100755 index 000000000..646753d93 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/specifications/DistributionSetSpecification.java @@ -0,0 +1,297 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.specifications; + +import java.util.Collection; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.JoinType; +import javax.persistence.criteria.ListJoin; +import javax.persistence.criteria.Path; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import javax.persistence.criteria.SetJoin; + +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetTag; +import org.eclipse.hawkbit.repository.model.DistributionSetTag_; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.DistributionSet_; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetInfo; +import org.eclipse.hawkbit.repository.model.TargetInfo_; +import org.eclipse.hawkbit.repository.model.Target_; +import org.springframework.data.jpa.domain.Specification; + +/** + * Specifications class for {@link DistributionSet}s. The class provides Spring + * Data JPQL Specifications + * + * + * + */ +public final class DistributionSetSpecification { + private DistributionSetSpecification() { + // utility class + } + + /** + * {@link Specification} for retrieving {@link DistributionSet}s by its + * DELETED attribute. + * + * @param isDeleted + * TRUE/FALSE are compared to the attribute DELETED. If NULL the + * attribute is ignored + * @return the {@link DistributionSet} {@link Specification} + */ + public static Specification isDeleted(final Boolean isDeleted) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final Predicate predicate = cb.equal(targetRoot. get(DistributionSet_.deleted), isDeleted); + return predicate; + } + }; + + return spec; + } + + /** + * {@link Specification} for retrieving {@link DistributionSet}s by its + * COMPLETED attribute. + * + * @param isCompleted + * TRUE/FALSE are compared to the attribute COMPLETED. If NULL + * the attribute is ignored + * @return the {@link DistributionSet} {@link Specification} + */ + public static Specification isCompleted(final Boolean isCompleted) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final Predicate predicate = cb.equal(targetRoot. get(DistributionSet_.complete), isCompleted); + return predicate; + } + }; + + return spec; + } + + /** + * {@link Specification} for retrieving {@link DistributionSet} with given + * {@link DistributionSet#getId()}. + * + * @param distid + * to search + * @return the {@link DistributionSet} {@link Specification} + */ + public static Specification byId(final Long distid) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + + final Predicate predicate = cb.equal(targetRoot. get(DistributionSet_.id), distid); + targetRoot.fetch(DistributionSet_.modules, JoinType.LEFT); + targetRoot.fetch(DistributionSet_.tags, JoinType.LEFT); + targetRoot.fetch(DistributionSet_.type, JoinType.LEFT); + query.distinct(true); + + return predicate; + } + }; + return spec; + } + + /** + * {@link Specification} for retrieving {@link DistributionSet} with given + * {@link DistributionSet#getId()}s. + * + * @param distids + * to search + * @return the {@link DistributionSet} {@link Specification} + */ + public static Specification byIds(final Collection distids) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + + final Predicate predicate = targetRoot. get(DistributionSet_.id).in(distids); + targetRoot.fetch(DistributionSet_.modules, JoinType.LEFT); + targetRoot.fetch(DistributionSet_.tags, JoinType.LEFT); + targetRoot.fetch(DistributionSet_.type, JoinType.LEFT); + return predicate; + } + }; + return spec; + } + + /** + * {@link Specification} for retrieving {@link DistributionSet}s by + * "like name or like description or like version". + * + * @param subString + * to be filtered on + * @return the {@link DistributionSet} {@link Specification} + */ + public static Specification likeNameOrDescriptionOrVersion(final String subString) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final Predicate predicate = cb.or( + cb.like(cb.lower(targetRoot. get(DistributionSet_.name)), subString.toLowerCase()), + cb.like(cb.lower(targetRoot. get(DistributionSet_.version)), subString.toLowerCase()), + cb.like(cb.lower(targetRoot. get(DistributionSet_.description)), + subString.toLowerCase())); + return predicate; + } + }; + + return spec; + } + + /** + * {@link Specification} for retrieving {@link DistributionSet}s by + * "has at least one of the given tag names". + * + * @param tagNames + * to be filtered on + * @param selectDSWithNoTag + * flag to select distribution sets with no tag + * @return the {@link DistributionSet} {@link Specification} + */ + public static Specification hasTags(final Collection tagNames, + final Boolean selectDSWithNoTag) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final SetJoin tags = targetRoot.join(DistributionSet_.tags, + JoinType.LEFT); + final Predicate predicate = getPredicate(tags, tagNames, selectDSWithNoTag, cb); + query.distinct(true); + return predicate; + } + }; + + return spec; + } + + private static Predicate getPredicate(final SetJoin tags, + final Collection tagNames, final Boolean selectDSWithNoTag, final CriteriaBuilder cb) { + tags.get(DistributionSetTag_.name); + final Path exp = tags.get(DistributionSetTag_.name); + if (selectDSWithNoTag != null && selectDSWithNoTag) { + if (tagNames != null) { + return cb.or(exp.isNull(), exp.in(tagNames)); + } else { + return exp.isNull(); + } + } else { + return exp.in(tagNames); + } + } + + /** + * returns query criteria {@link Specification} comparing case insensitive + * "NAME == AND VERSION ==". + * + * @param name + * to be filtered on + * @param version + * to be filtered on + * @return the {@link Specification} + */ + public static Specification equalsNameAndVersionIgnoreCase(final String name, final String version) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final Predicate predicate = cb.and( + // formatter:off + cb.equal(cb.lower(targetRoot. get(DistributionSet_.name)), name.toLowerCase()), + cb.equal(cb.lower(targetRoot. get(DistributionSet_.version)), version.toLowerCase())); + // formatter:on + return predicate; + } + }; + + return spec; + } + + /** + * {@link Specification} for retrieving {@link DistributionSet} with given + * {@link DistributionSet#getType()}. + * + * @param type + * to search + * @return the {@link DistributionSet} {@link Specification} + */ + public static Specification byType(final DistributionSetType type) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final Predicate predicate = cb.equal(targetRoot. get(DistributionSet_.type), type); + return predicate; + } + }; + + return spec; + } + + /** + * @param installedTargetId + * the targetID which is installed to a distribution set to + * search for. + * @return the specification to search for a distribution set which is + * installed to the given targetId + */ + public static Specification installedTarget(final String installedTargetId) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root dsRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final ListJoin installedTargetJoin = dsRoot.join( + DistributionSet_.installedAtTargets, JoinType.INNER); + final Join targetJoin = installedTargetJoin.join(TargetInfo_.target); + return cb.equal(targetJoin.get(Target_.controllerId), installedTargetId); + } + }; + + return spec; + } + + /** + * @param assignedTargetId + * the targetID which is assigned to a distribution set to search + * for. + * @return the specification to search for a distribution set which is + * assigned to the given targetId + */ + public static Specification assignedTarget(final String assignedTargetId) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root dsRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final ListJoin assignedTargetJoin = dsRoot.join( + DistributionSet_.assignedToTargets, JoinType.INNER); + return cb.equal(assignedTargetJoin.get(Target_.controllerId), assignedTargetId); + } + }; + return spec; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/specifications/DistributionSetTypeSpecification.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/specifications/DistributionSetTypeSpecification.java new file mode 100755 index 000000000..1caa5ef59 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/specifications/DistributionSetTypeSpecification.java @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.specifications; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.DistributionSetType_; +import org.springframework.data.jpa.domain.Specification; + +/** + * + * + * + * + * + */ +public final class DistributionSetTypeSpecification { + private DistributionSetTypeSpecification() { + // utility class + } + + /** + * {@link Specification} for retrieving {@link DistributionSetType}s by its + * DELETED attribute. + * + * @param isDeleted + * TRUE/FALSE are compared to the attribute DELETED. If NULL the + * attribute is ignored + * @return the {@link DistributionSetType} {@link Specification} + */ + public static Specification isDeleted(final Boolean isDeleted) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final Predicate predicate = cb.equal(targetRoot. get(DistributionSetType_.deleted), isDeleted); + return predicate; + } + }; + + return spec; + } + + /** + * {@link Specification} for retrieving {@link DistributionSetType} with + * given {@link DistributionSetType#getId()} including fetching the elements + * list. + * + * @param distid + * to search + * @return the {@link DistributionSet} {@link Specification} + */ + public static Specification byId(final Long distid) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + + final Predicate predicate = cb.equal(targetRoot. get(DistributionSetType_.id), distid); + return predicate; + } + }; + return spec; + } + + /** + * {@link Specification} for retrieving {@link DistributionSetType} with + * given {@link DistributionSetType#getName())} including fetching the + * elements list. + * + * @param name + * to search + * @return the {@link DistributionSet} {@link Specification} + */ + public static Specification byName(final String name) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + + final Predicate predicate = cb.equal(targetRoot. get(DistributionSetType_.name), name); + return predicate; + } + }; + return spec; + } + + /** + * {@link Specification} for retrieving {@link DistributionSetType} with + * given {@link DistributionSetType#getKey()} including fetching the + * elements list. + * + * @param key + * to search + * @return the {@link DistributionSet} {@link Specification} + */ + public static Specification byKey(final String key) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + + final Predicate predicate = cb.equal(targetRoot. get(DistributionSetType_.key), key); + return predicate; + } + }; + return spec; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/specifications/SoftwareModuleSpecification.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/specifications/SoftwareModuleSpecification.java new file mode 100755 index 000000000..2311f59f6 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/specifications/SoftwareModuleSpecification.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.specifications; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.repository.model.SoftwareModule_; +import org.springframework.data.jpa.domain.Specification; + +/** + * Specifications class for {@link SoftwareModule}s. The class provides Spring + * Data JPQL Specifications + * + * + * + */ +public final class SoftwareModuleSpecification { + private SoftwareModuleSpecification() { + // utility class + } + + /** + * {@link Specification} for retrieving {@link SoftwareModule}s by its ID + * attribute. + * + * @param moduleId + * to search for + * @return the {@link SoftwareModule} {@link Specification} + */ + public static Specification byId(final Long moduleId) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + + final Predicate predicate = cb.equal(targetRoot. get(SoftwareModule_.id), moduleId); + targetRoot.fetch(SoftwareModule_.type); + return predicate; + } + }; + return spec; + } + + /** + * {@link Specification} for retrieving {@link SoftwareModule}s by its + * DELETED attribute. + * + * @param isDeleted + * TRUE/FALSE are compared to the attribute DELETED. If NULL the + * attribute is ignored + * @return the {@link SoftwareModule} {@link Specification} + */ + public static Specification isDeletedFalse() { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root swRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final Predicate predicate = cb.equal(swRoot. get(SoftwareModule_.deleted), Boolean.FALSE); + return predicate; + } + }; + return spec; + } + + /** + * {@link Specification} for retrieving {@link SoftwareModule}s by + * "like name or like version". + * + * @param subString + * to be filtered on + * @return the {@link SoftwareModule} {@link Specification} + */ + public static Specification likeNameOrVersion(final String subString) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final Predicate predicate = cb.or( + cb.like(cb.lower(targetRoot. get(SoftwareModule_.name)), subString.toLowerCase()), + cb.like(cb.lower(targetRoot. get(SoftwareModule_.version)), subString.toLowerCase())); + return predicate; + } + }; + + return spec; + } + + /** + * {@link Specification} for retrieving {@link SoftwareModule}s by + * "like name or like version". + * + * @param type + * to be filtered on + * @return the {@link SoftwareModule} {@link Specification} + */ + public static Specification equalType(final SoftwareModuleType type) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final Predicate predicate = cb.equal(targetRoot. get(SoftwareModule_.type), type); + return predicate; + } + }; + + return spec; + } + +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/specifications/TargetFilterQuerySpecification.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/specifications/TargetFilterQuerySpecification.java new file mode 100755 index 000000000..c92da7ed1 --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/specifications/TargetFilterQuerySpecification.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.specifications; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + +import org.eclipse.hawkbit.repository.model.TargetFilterQuery; +import org.eclipse.hawkbit.repository.model.TargetFilterQuery_; +import org.springframework.data.jpa.domain.Specification; + +/** + * + * + */ +public class TargetFilterQuerySpecification { + private TargetFilterQuerySpecification() { + // utility class + } + + public static Specification likeName(final String searchText) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetFilterQueryRoot, + final CriteriaQuery query, final CriteriaBuilder cb) { + final String searchTextToLower = searchText.toLowerCase(); + final Predicate predicate = cb.like(cb.lower(targetFilterQueryRoot.get(TargetFilterQuery_.name)), + searchTextToLower); + return predicate; + } + }; + return spec; + } +} diff --git a/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/specifications/TargetSpecifications.java b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/specifications/TargetSpecifications.java new file mode 100755 index 000000000..0c91a1cce --- /dev/null +++ b/hawkbit-repository/src/main/java/org/eclipse/hawkbit/repository/specifications/TargetSpecifications.java @@ -0,0 +1,281 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.specifications; + +import java.util.Collection; +import java.util.List; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.JoinType; +import javax.persistence.criteria.Path; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import javax.persistence.criteria.SetJoin; +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSet_; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetInfo; +import org.eclipse.hawkbit.repository.model.TargetInfo_; +import org.eclipse.hawkbit.repository.model.TargetTag; +import org.eclipse.hawkbit.repository.model.TargetTag_; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.eclipse.hawkbit.repository.model.Target_; +import org.springframework.data.jpa.domain.Specification; + +/** + * Specifications class for {@link Target}s. The class provides Spring Data JPQL + * Specifications. + * + * + * + */ +public final class TargetSpecifications { + private TargetSpecifications() { + // utility class + } + + /** + * {@link Specification} for retrieving {@link Target}s including + * {@link TargetTag}s and {@link TargetStatus}. + * + * @param controllerIDs + * to search for + * + * @return the {@link Target} {@link Specification} + */ + public static Specification byControllerIdWithStatusAndTagsInJoin(final Collection controllerIDs) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final Predicate predicate = targetRoot.get(Target_.controllerId).in(controllerIDs); + targetRoot.fetch(Target_.tags, JoinType.LEFT); + query.distinct(true); + return predicate; + } + }; + + return spec; + } + + /** + * {@link Specification} for retrieving {@link Target}s including { + * {@link TargetStatus}. + * + * @param controllerIDs + * to search for + * + * @return the {@link Target} {@link Specification} + */ + public static Specification byControllerIdWithStatusAndAssignedInJoin(final Collection controllerIDs) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + + final Predicate predicate = targetRoot.get(Target_.controllerId).in(controllerIDs); + targetRoot.fetch(Target_.assignedDistributionSet); + return predicate; + } + }; + + return spec; + } + + /** + * {@link Specification} for retrieving {@link Target}s by "equal to given + * {@link TargetUpdateStatus}". + * + * @param updateStatus + * to be filtered on + * @param fetch + * {@code true} to fetch the {@link TargetInfo} otherwise only + * join it. + * @return the {@link Target} {@link Specification} + */ + public static Specification hasTargetUpdateStatus(final Collection updateStatus, + final boolean fetch) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + if (!query.getResultType().isAssignableFrom(Long.class)) { + if (fetch) { + targetRoot.fetch(Target_.targetInfo); + } else { + targetRoot.join(Target_.targetInfo); + } + return targetRoot.get(Target_.targetInfo).get(TargetInfo_.updateStatus).in(updateStatus); + } + final Join targetInfoJoin = targetRoot.join(Target_.targetInfo); + return targetInfoJoin.get(TargetInfo_.updateStatus).in(updateStatus); + } + }; + + return spec; + } + + /** + * {@link Specification} for retrieving {@link Target}s by + * "like controllerId or like description or like ip address". + * + * @param searchText + * to be filtered on + * @return the {@link Target} {@link Specification} + */ + public static Specification likeNameOrDescriptionOrIp(final String searchText) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final String searchTextToLower = searchText.toLowerCase(); + final Predicate predicate = cb.or(cb.like(cb.lower(targetRoot.get(Target_.name)), searchTextToLower), + cb.like(cb.lower(targetRoot.get(Target_.description)), searchTextToLower)); + return predicate; + } + }; + + return spec; + } + + /** + * {@link Specification} for retrieving {@link Target}s by + * "like controllerId". + * + * @param distributionId + * to be filtered on + * @return the {@link Target} {@link Specification} + */ + public static Specification hasInstalledOrAssignedDistributionSet(@NotNull final Long distributionId) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final Join targetInfoJoin = targetRoot.join(Target_.targetInfo); + final Predicate predicate = cb.or(cb.equal(targetInfoJoin.get(TargetInfo_.installedDistributionSet) + .get(DistributionSet_.id), distributionId), cb.equal( + targetRoot. get(Target_.assignedDistributionSet).get(DistributionSet_.id), + distributionId)); + return predicate; + } + }; + + return spec; + } + + /** + * Finds all targets by given {@link Target#getControllerId()}s and which + * are not yet assigned to given {@link DistributionSet}. + * + * @param tIDs + * to search for. + * @param distributionId + * set that is not yet assigned + * @return the {@link Target} {@link Specification} + */ + public static Specification hasControllerIdAndAssignedDistributionSetIdNot(final List tIDs, + @NotNull final Long distributionId) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final Predicate predicate = cb.and( + targetRoot.get(Target_.controllerId).in(tIDs), + cb.or(cb.notEqual( + targetRoot. get(Target_.assignedDistributionSet).get( + DistributionSet_.id), distributionId), + cb.isNull(targetRoot. get(Target_.assignedDistributionSet)))); + return predicate; + } + }; + return spec; + } + + /** + * {@link Specification} for retrieving {@link Target}s by + * "has no tag names"or "has at least on of the given tag names". + * + * @param tagNames + * to be filtered on + * @param selectTargetWithNoTag + * flag to get targets with no tag assigned + * @return the {@link Target} {@link Specification} + */ + public static Specification hasTags(final String[] tagNames, final Boolean selectTargetWithNoTag) { + final Specification spec = new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final Predicate predicate = getPredicate(targetRoot, cb, selectTargetWithNoTag, tagNames); + query.distinct(true); + return predicate; + } + }; + return spec; + } + + private static Predicate getPredicate(final Root targetRoot, final CriteriaBuilder cb, + final Boolean selectTargetWithNoTag, final String[] tagNames) { + final SetJoin tags = targetRoot.join(Target_.tags, JoinType.LEFT); + final Path exp = tags.get(TargetTag_.name); + if (selectTargetWithNoTag) { + if (tagNames != null) { + return cb.or(exp.isNull(), exp.in(tagNames)); + } else { + return exp.isNull(); + } + } else { + return exp.in(tagNames); + } + } + + /** + * {@link Specification} for retrieving {@link Target}s by assigned + * distribution set. + * + * @param distributionSetId + * the ID of the distribution set which must be assigned + * @return the {@link Target} {@link Specification} + */ + public static Specification hasAssignedDistributionSet(final Long distributionSetId) { + return new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + return cb.equal( + targetRoot. get(Target_.assignedDistributionSet).get(DistributionSet_.id), + distributionSetId); + } + }; + } + + /** + * {@link Specification} for retrieving {@link Target}s by assigned + * distribution set. + * + * @param distributionSetId + * the ID of the distribution set which must be assigned + * @return the {@link Target} {@link Specification} + */ + public static Specification hasInstalledDistributionSet(final Long distributionSetId) { + return new Specification() { + @Override + public Predicate toPredicate(final Root targetRoot, final CriteriaQuery query, + final CriteriaBuilder cb) { + final Join targetInfoJoin = targetRoot.join(Target_.targetInfo); + return cb.equal(targetInfoJoin.get(TargetInfo_.installedDistributionSet).get(DistributionSet_.id), + distributionSetId); + } + }; + } +} diff --git a/hawkbit-repository/src/main/resources/db/migration/H2/V1_0_1__init___H2.sql b/hawkbit-repository/src/main/resources/db/migration/H2/V1_0_1__init___H2.sql new file mode 100755 index 000000000..5f022bb3c --- /dev/null +++ b/hawkbit-repository/src/main/resources/db/migration/H2/V1_0_1__init___H2.sql @@ -0,0 +1,494 @@ + + create table sp_action ( + id bigint generated by default as identity, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + action_type varchar(255) not null, + active boolean, + forced_time bigint, + status integer, + distribution_set bigint, + target bigint, + primary key (id) + ); + + create table sp_action_status ( + id bigint generated by default as identity, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + target_occurred_at bigint, + status integer, + action bigint not null, + primary key (id) + ); + + create table sp_action_status_messages ( + action_status_id bigint not null, + detail_message varchar(512) + ); + + create table sp_artifact ( + id bigint generated by default as identity, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + md5_hash varchar(32), + sha1_hash varchar(40), + file_size bigint, + provided_file_name varchar(256), + gridfs_file_name varchar(40), + software_module bigint not null, + primary key (id) + ); + + create table sp_base_software_module ( + id bigint generated by default as identity, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + version varchar(64) not null, + deleted boolean, + vendor varchar(256), + module_type bigint not null, + primary key (id) + ); + + create table sp_distribution_set ( + id bigint generated by default as identity, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + version varchar(64) not null, + complete boolean, + deleted boolean, + required_migration_step boolean, + ds_id bigint not null, + primary key (id) + ); + + create table sp_distribution_set_type ( + id bigint generated by default as identity, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + colour varchar(16), + deleted boolean, + type_key varchar(64) not null, + primary key (id) + ); + + create table sp_distributionset_tag ( + id bigint generated by default as identity, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + colour varchar(16), + primary key (id) + ); + + create table sp_ds_dstag ( + ds bigint not null, + TAG bigint not null, + primary key (ds, TAG) + ); + + create table sp_ds_metadata ( + meta_key varchar(128) not null, + meta_value varchar(4000), + ds_id bigint not null, + primary key (ds_id, meta_key) + ); + + create table sp_ds_module ( + ds_id bigint not null, + module_id bigint not null, + primary key (ds_id, module_id) + ); + + create table sp_ds_type_element ( + mandatory boolean, + distribution_set_type bigint not null, + software_module_type bigint not null, + primary key (distribution_set_type, software_module_type) + ); + + create table sp_external_artifact ( + id bigint generated by default as identity, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + md5_hash varchar(32), + sha1_hash varchar(40), + file_size bigint, + url_suffix varchar(512), + provider bigint not null, + software_module bigint not null, + primary key (id) + ); + + create table sp_external_provider ( + id bigint generated by default as identity, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + base_url varchar(512) not null, + default_url_suffix varchar(512), + primary key (id) + ); + + create table sp_software_module_type ( + id bigint generated by default as identity, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + colour varchar(16), + deleted boolean, + type_key varchar(64) not null, + max_ds_assignments integer not null, + primary key (id) + ); + + create table sp_sw_metadata ( + meta_key varchar(128) not null, + meta_value varchar(4000), + sw_id bigint not null, + primary key (meta_key, sw_id) + ); + + create table sp_target ( + id bigint generated by default as identity, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + controller_id varchar(64), + sec_token varchar(128) not null, + assigned_distribution_set bigint, + primary key (id) + ); + + create table sp_target_attributes ( + target_id bigint not null, + attribute_value varchar(128), + attribute_key varchar(32) not null, + primary key (target_id, attribute_key) + ); + + create table sp_target_info ( + target_id bigint not null, + install_date bigint, + ip_address varchar(46), + last_target_query bigint, + request_controller_attributes boolean not null, + update_status varchar(255) not null, + installed_distribution_set bigint, + primary key (target_id) + ); + + create table sp_target_tag ( + id bigint generated by default as identity, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + colour varchar(16), + primary key (id) + ); + + create table sp_target_target_tag ( + target bigint not null, + tag bigint not null, + primary key (target, tag) + ); + + create table sp_tenant ( + id bigint generated by default as identity, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + default_ds_type bigint not null, + primary key (id) + ); + + create table sp_tenant_configuration ( + id bigint generated by default as identity, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + conf_key varchar(128), + conf_value varchar(512), + primary key (id) + ); + + create index sp_idx_action_01 on sp_action (tenant, distribution_set); + + create index sp_idx_action_02 on sp_action (tenant, target, active); + + create index sp_idx_action_prim on sp_action (tenant, id); + + create index sp_idx_action_status_01 on sp_action_status (tenant, action); + + create index sp_idx_action_status_02 on sp_action_status (tenant, action, status); + + create index sp_idx_action_status_prim on sp_action_status (tenant, id); + + create index sp_idx_action_status_msgs_01 on sp_action_status_messages (action_status_id); + + create index sp_idx_artifact_01 on sp_artifact (tenant, software_module); + + create index sp_idx_artifact_prim on sp_artifact (tenant, id); + + alter table sp_base_software_module + add constraint uk_base_sw_mod unique (module_type, name, version, tenant); + + create index sp_idx_base_sw_module_01 on sp_base_software_module (tenant, deleted, name, version); + + create index sp_idx_base_sw_module_02 on sp_base_software_module (tenant, deleted, module_type); + + create index sp_idx_base_sw_module_prim on sp_base_software_module (tenant, id); + + alter table sp_distribution_set + add constraint uk_distrib_set unique (name, version, tenant); + + create index sp_idx_distribution_set_01 on sp_distribution_set (tenant, deleted, name, complete); + + create index sp_idx_distribution_set_02 on sp_distribution_set (tenant, required_migration_step); + + create index sp_idx_distribution_set_prim on sp_distribution_set (tenant, id); + + alter table sp_distribution_set_type + add constraint uk_dst_name unique (name, tenant); + + alter table sp_distribution_set_type + add constraint uk_dst_key unique (type_key, tenant); + + create index sp_idx_distribution_set_type_01 on sp_distribution_set_type (tenant, deleted); + + create index sp_idx_distribution_set_type_prim on sp_distribution_set_type (tenant, id); + + alter table sp_distributionset_tag + add constraint uk_ds_tag unique (name, tenant); + + create index sp_idx_distribution_set_tag_prim on sp_distributionset_tag (tenant, id); + + create index sp_idx_external_artifact_prim on sp_external_artifact (id, tenant); + + create index sp_idx_external_provider_prim on sp_external_provider (tenant, id); + + alter table sp_software_module_type + add constraint uk_smt_type_key unique (type_key, tenant); + + alter table sp_software_module_type + add constraint uk_smt_name unique (name, tenant); + + create index sp_idx_software_module_type_01 on sp_software_module_type (tenant, deleted); + + create index sp_idx_software_module_type_prim on sp_software_module_type (tenant, id); + + alter table sp_target + add constraint uk_tenant_controller_id unique (controller_id, tenant); + + create index sp_idx_target_01 on sp_target (tenant, name, assigned_distribution_set); + + create index sp_idx_target_02 on sp_target (tenant, name); + + create index sp_idx_target_03 on sp_target (tenant, controller_id, assigned_distribution_set); + + create index sp_idx_target_04 on sp_target (tenant, created_at); + + create index sp_idx_target_prim on sp_target (tenant, id); + + create index sp_idx_target_info_01 on sp_target_info (ip_address); + + create index sp_idx_target_info_02 on sp_target_info (target_id, update_status); + + alter table sp_target_tag + add constraint uk_targ_tag unique (name, tenant); + + create index sp_idx_target_tag_prim on sp_target_tag (tenant, id); + + alter table sp_tenant + add constraint uk_tenantmd_tenant unique (tenant); + + create index sp_idx_tenant_prim on sp_tenant (tenant, id); + + alter table sp_tenant_configuration + add constraint uk_tenant_key unique (conf_key, tenant); + + alter table sp_action + add constraint fk_action_ds + foreign key (distribution_set) + references sp_distribution_set; + + alter table sp_action + add constraint fk_targ_act_hist_targ + foreign key (target) + references sp_target; + + alter table sp_action_status + add constraint fk_act_stat_action + foreign key (action) + references sp_action; + + alter table sp_action_status_messages + add constraint fk_stat_msg_act_stat + foreign key (action_status_id) + references sp_action_status; + + alter table sp_artifact + add constraint fk_assigned_sm + foreign key (software_module) + references sp_base_software_module; + + alter table sp_base_software_module + add constraint fk_module_type + foreign key (module_type) + references sp_software_module_type; + + alter table sp_distribution_set + add constraint fk_ds_dstype_ds + foreign key (ds_id) + references sp_distribution_set_type; + + alter table sp_ds_dstag + add constraint fk_ds_dstag_tag + foreign key (TAG) + references sp_distributionset_tag; + + alter table sp_ds_dstag + add constraint fk_ds_dstag_ds + foreign key (ds) + references sp_distribution_set; + + alter table sp_ds_metadata + add constraint fk_metadata_ds + foreign key (ds_id) + references sp_distribution_set; + + alter table sp_ds_module + add constraint fk_ds_module_module + foreign key (module_id) + references sp_base_software_module; + + alter table sp_ds_module + add constraint fk_ds_module_ds + foreign key (ds_id) + references sp_distribution_set; + + alter table sp_ds_type_element + add constraint fk_ds_type_element_element + foreign key (distribution_set_type) + references sp_distribution_set_type; + + alter table sp_ds_type_element + add constraint fk_ds_type_element_smtype + foreign key (software_module_type) + references sp_software_module_type; + + alter table sp_external_artifact + add constraint fk_art_to_ext_provider + foreign key (provider) + references sp_external_provider; + + alter table sp_external_artifact + add constraint fk_external_assigned_sm + foreign key (software_module) + references sp_base_software_module; + + alter table sp_sw_metadata + add constraint fk_metadata_sw + foreign key (sw_id) + references sp_base_software_module; + + alter table sp_target + add constraint fk_target_assign_ds + foreign key (assigned_distribution_set) + references sp_distribution_set; + + alter table sp_target_attributes + add constraint fk_targ_attrib_target + foreign key (target_id) + references sp_target_info; + + alter table sp_target_info + add constraint fk_target_inst_ds + foreign key (installed_distribution_set) + references sp_distribution_set; + + alter table sp_target_info + add constraint fk_targ_stat_targ + foreign key (target_id) + references sp_target; + + alter table sp_target_target_tag + add constraint fk_targ_targtag_tag + foreign key (tag) + references sp_target_tag; + + alter table sp_target_target_tag + add constraint fk_targ_targtag_target + foreign key (target) + references sp_target; + + alter table sp_tenant + add constraint fk_tenant_md_default_ds_type + foreign key (default_ds_type) + references sp_distribution_set_type; diff --git a/hawkbit-repository/src/main/resources/db/migration/H2/V1_2_0__update_target_info_for_message___H2.sql b/hawkbit-repository/src/main/resources/db/migration/H2/V1_2_0__update_target_info_for_message___H2.sql new file mode 100755 index 000000000..5bf96e631 --- /dev/null +++ b/hawkbit-repository/src/main/resources/db/migration/H2/V1_2_0__update_target_info_for_message___H2.sql @@ -0,0 +1,13 @@ +DROP INDEX sp_idx_target_info_01; + +ALTER TABLE sp_target_info ALTER COLUMN ip_address RENAME TO address; +ALTER TABLE sp_target_info ALTER COLUMN address VARCHAR(512); + +UPDATE sp_target_info +SET address = CONCAT('http://',(SELECT address + FROM sp_target_info i + WHERE sp_target_info.target_id = i.target_id)) +WHERE EXISTS(SELECT target_id + FROM sp_target_info i + WHERE sp_target_info.target_id = i.target_id) +AND address != null; \ No newline at end of file diff --git a/hawkbit-repository/src/main/resources/db/migration/H2/V1_4_0__cascade_delete___H2.sql b/hawkbit-repository/src/main/resources/db/migration/H2/V1_4_0__cascade_delete___H2.sql new file mode 100755 index 000000000..7ed5e8d58 --- /dev/null +++ b/hawkbit-repository/src/main/resources/db/migration/H2/V1_4_0__cascade_delete___H2.sql @@ -0,0 +1,69 @@ +alter table sp_target_info drop constraint fk_targ_stat_targ; +alter table sp_target_info + add constraint fk_targ_stat_targ + foreign key (target_id) + references sp_target + on delete cascade; + +alter table sp_action drop constraint fk_targ_act_hist_targ; +alter table sp_action + add constraint fk_targ_act_hist_targ + foreign key (target) + references sp_target + on delete cascade; + +alter table sp_action_status drop constraint fk_act_stat_action; +alter table sp_action_status + add constraint fk_act_stat_action + foreign key (action) + references sp_action + on delete cascade; + +alter table sp_sw_metadata drop constraint fk_metadata_sw; +alter table sp_sw_metadata + add constraint fk_metadata_sw + foreign key (sw_id) + references sp_base_software_module + on delete cascade; + +alter table sp_ds_metadata drop constraint fk_metadata_ds; +alter table sp_ds_metadata + add constraint fk_metadata_ds + foreign key (ds_id) + references sp_distribution_set + on delete cascade; + +alter table sp_action_status_messages drop constraint fk_stat_msg_act_stat; +alter table sp_action_status_messages + add constraint fk_stat_msg_act_stat + foreign key (action_status_id) + references sp_action_status + on delete cascade; + +alter table sp_ds_dstag drop constraint fk_ds_dstag_tag; +alter table sp_ds_dstag + add constraint fk_ds_dstag_tag + foreign key (TAG) + references sp_distributionset_tag + on delete cascade; + +alter table sp_ds_dstag drop constraint fk_ds_dstag_ds; +alter table sp_ds_dstag + add constraint fk_ds_dstag_ds + foreign key (ds) + references sp_distribution_set + on delete cascade; + +alter table sp_target_target_tag drop constraint fk_targ_targtag_tag; +alter table sp_target_target_tag + add constraint fk_targ_targtag_tag + foreign key (tag) + references sp_target_tag + on delete cascade; + +alter table sp_target_target_tag drop constraint fk_targ_targtag_target; +alter table sp_target_target_tag + add constraint fk_targ_targtag_target + foreign key (target) + references sp_target + on delete cascade; \ No newline at end of file diff --git a/hawkbit-repository/src/main/resources/db/migration/H2/V1_4_1__cascade_delete___H2.sql b/hawkbit-repository/src/main/resources/db/migration/H2/V1_4_1__cascade_delete___H2.sql new file mode 100755 index 000000000..915dac153 --- /dev/null +++ b/hawkbit-repository/src/main/resources/db/migration/H2/V1_4_1__cascade_delete___H2.sql @@ -0,0 +1,6 @@ +alter table sp_target_attributes drop constraint fk_targ_attrib_target; +alter table sp_target_attributes + add constraint fk_targ_attrib_target + foreign key (target_id) + references sp_target_info + on delete cascade; \ No newline at end of file diff --git a/hawkbit-repository/src/main/resources/db/migration/H2/V1_5_0__target_filter_query___H2.sql b/hawkbit-repository/src/main/resources/db/migration/H2/V1_5_0__target_filter_query___H2.sql new file mode 100755 index 000000000..0cbc3744a --- /dev/null +++ b/hawkbit-repository/src/main/resources/db/migration/H2/V1_5_0__target_filter_query___H2.sql @@ -0,0 +1,14 @@ + create table sp_target_filter_query ( + id bigint generated by default as identity, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + name varchar(64) not null, + query varchar(1024) not null, + primary key (id) +); + +create index sp_idx_target_filter_query_01 on sp_target_filter_query (tenant, name); \ No newline at end of file diff --git a/hawkbit-repository/src/main/resources/db/migration/MYSQL/V1_0_1__init___MYSQL.sql b/hawkbit-repository/src/main/resources/db/migration/MYSQL/V1_0_1__init___MYSQL.sql new file mode 100755 index 000000000..ec243a68b --- /dev/null +++ b/hawkbit-repository/src/main/resources/db/migration/MYSQL/V1_0_1__init___MYSQL.sql @@ -0,0 +1,494 @@ + + create table sp_action ( + id bigint not null auto_increment, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + action_type varchar(255) not null, + active bit, + forced_time bigint, + status integer, + distribution_set bigint, + target bigint, + primary key (id) + ); + + create table sp_action_status ( + id bigint not null auto_increment, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + target_occurred_at bigint, + status integer, + action bigint not null, + primary key (id) + ); + + create table sp_action_status_messages ( + action_status_id bigint not null, + detail_message varchar(512) + ); + + create table sp_artifact ( + id bigint not null auto_increment, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + md5_hash varchar(32), + sha1_hash varchar(40), + file_size bigint, + provided_file_name varchar(256), + gridfs_file_name varchar(40), + software_module bigint not null, + primary key (id) + ); + + create table sp_base_software_module ( + id bigint not null auto_increment, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + version varchar(64) not null, + deleted bit, + vendor varchar(256), + module_type bigint not null, + primary key (id) + ); + + create table sp_distribution_set ( + id bigint not null auto_increment, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + version varchar(64) not null, + complete bit, + deleted bit, + required_migration_step bit, + ds_id bigint not null, + primary key (id) + ); + + create table sp_distribution_set_type ( + id bigint not null auto_increment, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + colour varchar(16), + deleted bit, + type_key varchar(64) not null, + primary key (id) + ); + + create table sp_distributionset_tag ( + id bigint not null auto_increment, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + colour varchar(16), + primary key (id) + ); + + create table sp_ds_dstag ( + ds bigint not null, + TAG bigint not null, + primary key (ds, TAG) + ); + + create table sp_ds_metadata ( + meta_key varchar(128) not null, + meta_value varchar(4000), + ds_id bigint not null, + primary key (ds_id, meta_key) + ); + + create table sp_ds_module ( + ds_id bigint not null, + module_id bigint not null, + primary key (ds_id, module_id) + ); + + create table sp_ds_type_element ( + mandatory bit, + distribution_set_type bigint not null, + software_module_type bigint not null, + primary key (distribution_set_type, software_module_type) + ); + + create table sp_external_artifact ( + id bigint not null auto_increment, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + md5_hash varchar(32), + sha1_hash varchar(40), + file_size bigint, + url_suffix varchar(512), + provider bigint not null, + software_module bigint not null, + primary key (id) + ); + + create table sp_external_provider ( + id bigint not null auto_increment, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + base_url varchar(512) not null, + default_url_suffix varchar(512), + primary key (id) + ); + + create table sp_software_module_type ( + id bigint not null auto_increment, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + colour varchar(16), + deleted bit, + type_key varchar(64) not null, + max_ds_assignments integer not null, + primary key (id) + ); + + create table sp_sw_metadata ( + meta_key varchar(128) not null, + meta_value varchar(4000), + sw_id bigint not null, + primary key (meta_key, sw_id) + ); + + create table sp_target ( + id bigint not null auto_increment, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + controller_id varchar(64), + sec_token varchar(128) not null, + assigned_distribution_set bigint, + primary key (id) + ); + + create table sp_target_attributes ( + target_id bigint not null, + attribute_value varchar(128), + attribute_key varchar(32) not null, + primary key (target_id, attribute_key) + ); + + create table sp_target_info ( + target_id bigint not null, + install_date bigint, + ip_address varchar(46), + last_target_query bigint, + request_controller_attributes bit not null, + update_status varchar(255) not null, + installed_distribution_set bigint, + primary key (target_id) + ); + + create table sp_target_tag ( + id bigint not null auto_increment, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + colour varchar(16), + primary key (id) + ); + + create table sp_target_target_tag ( + target bigint not null, + tag bigint not null, + primary key (target, tag) + ); + + create table sp_tenant ( + id bigint not null auto_increment, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + default_ds_type bigint not null, + primary key (id) + ); + + create table sp_tenant_configuration ( + id bigint not null auto_increment, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + conf_key varchar(128), + conf_value varchar(512), + primary key (id) + ); + + create index sp_idx_action_01 on sp_action (tenant, distribution_set); + + create index sp_idx_action_02 on sp_action (tenant, target, active); + + create index sp_idx_action_prim on sp_action (tenant, id); + + create index sp_idx_action_status_01 on sp_action_status (tenant, action); + + create index sp_idx_action_status_02 on sp_action_status (tenant, action, status); + + create index sp_idx_action_status_prim on sp_action_status (tenant, id); + + create index sp_idx_action_status_msgs_01 on sp_action_status_messages (action_status_id); + + create index sp_idx_artifact_01 on sp_artifact (tenant, software_module); + + create index sp_idx_artifact_prim on sp_artifact (tenant, id); + + alter table sp_base_software_module + add constraint uk_base_sw_mod unique (module_type, name, version, tenant); + + create index sp_idx_base_sw_module_01 on sp_base_software_module (tenant, deleted, name, version); + + create index sp_idx_base_sw_module_02 on sp_base_software_module (tenant, deleted, module_type); + + create index sp_idx_base_sw_module_prim on sp_base_software_module (tenant, id); + + alter table sp_distribution_set + add constraint uk_distrib_set unique (name, version, tenant); + + create index sp_idx_distribution_set_01 on sp_distribution_set (tenant, deleted, name, complete); + + create index sp_idx_distribution_set_02 on sp_distribution_set (tenant, required_migration_step); + + create index sp_idx_distribution_set_prim on sp_distribution_set (tenant, id); + + alter table sp_distribution_set_type + add constraint uk_dst_name unique (name, tenant); + + alter table sp_distribution_set_type + add constraint uk_dst_key unique (type_key, tenant); + + create index sp_idx_distribution_set_type_01 on sp_distribution_set_type (tenant, deleted); + + create index sp_idx_distribution_set_type_prim on sp_distribution_set_type (tenant, id); + + alter table sp_distributionset_tag + add constraint uk_ds_tag unique (name, tenant); + + create index sp_idx_distribution_set_tag_prim on sp_distributionset_tag (tenant, id); + + create index sp_idx_external_artifact_prim on sp_external_artifact (id, tenant); + + create index sp_idx_external_provider_prim on sp_external_provider (tenant, id); + + alter table sp_software_module_type + add constraint uk_smt_type_key unique (type_key, tenant); + + alter table sp_software_module_type + add constraint uk_smt_name unique (name, tenant); + + create index sp_idx_software_module_type_01 on sp_software_module_type (tenant, deleted); + + create index sp_idx_software_module_type_prim on sp_software_module_type (tenant, id); + + alter table sp_target + add constraint uk_tenant_controller_id unique (controller_id, tenant); + + create index sp_idx_target_01 on sp_target (tenant, name, assigned_distribution_set); + + create index sp_idx_target_02 on sp_target (tenant, name); + + create index sp_idx_target_03 on sp_target (tenant, controller_id, assigned_distribution_set); + + create index sp_idx_target_04 on sp_target (tenant, created_at); + + create index sp_idx_target_prim on sp_target (tenant, id); + + create index sp_idx_target_info_01 on sp_target_info (ip_address); + + create index sp_idx_target_info_02 on sp_target_info (target_id, update_status); + + alter table sp_target_tag + add constraint uk_targ_tag unique (name, tenant); + + create index sp_idx_target_tag_prim on sp_target_tag (tenant, id); + + alter table sp_tenant + add constraint uk_tenantmd_tenant unique (tenant); + + create index sp_idx_tenant_prim on sp_tenant (tenant, id); + + alter table sp_tenant_configuration + add constraint uk_tenant_key unique (conf_key, tenant); + + alter table sp_action + add constraint fk_action_ds + foreign key (distribution_set) + references sp_distribution_set (id); + + alter table sp_action + add constraint fk_targ_act_hist_targ + foreign key (target) + references sp_target (id); + + alter table sp_action_status + add constraint fk_act_stat_action + foreign key (action) + references sp_action (id); + + alter table sp_action_status_messages + add constraint fk_stat_msg_act_stat + foreign key (action_status_id) + references sp_action_status (id); + + alter table sp_artifact + add constraint fk_assigned_sm + foreign key (software_module) + references sp_base_software_module (id); + + alter table sp_base_software_module + add constraint fk_module_type + foreign key (module_type) + references sp_software_module_type (id); + + alter table sp_distribution_set + add constraint fk_ds_dstype_ds + foreign key (ds_id) + references sp_distribution_set_type (id); + + alter table sp_ds_dstag + add constraint fk_ds_dstag_tag + foreign key (TAG) + references sp_distributionset_tag (id); + + alter table sp_ds_dstag + add constraint fk_ds_dstag_ds + foreign key (ds) + references sp_distribution_set (id); + + alter table sp_ds_metadata + add constraint fk_metadata_ds + foreign key (ds_id) + references sp_distribution_set (id); + + alter table sp_ds_module + add constraint fk_ds_module_module + foreign key (module_id) + references sp_base_software_module (id); + + alter table sp_ds_module + add constraint fk_ds_module_ds + foreign key (ds_id) + references sp_distribution_set (id); + + alter table sp_ds_type_element + add constraint fk_ds_type_element_element + foreign key (distribution_set_type) + references sp_distribution_set_type (id); + + alter table sp_ds_type_element + add constraint fk_ds_type_element_smtype + foreign key (software_module_type) + references sp_software_module_type (id); + + alter table sp_external_artifact + add constraint fk_art_to_ext_provider + foreign key (provider) + references sp_external_provider (id); + + alter table sp_external_artifact + add constraint fk_external_assigned_sm + foreign key (software_module) + references sp_base_software_module (id); + + alter table sp_sw_metadata + add constraint fk_metadata_sw + foreign key (sw_id) + references sp_base_software_module (id); + + alter table sp_target + add constraint fk_target_assign_ds + foreign key (assigned_distribution_set) + references sp_distribution_set (id); + + alter table sp_target_attributes + add constraint fk_targ_attrib_target + foreign key (target_id) + references sp_target_info (target_id); + + alter table sp_target_info + add constraint fk_target_inst_ds + foreign key (installed_distribution_set) + references sp_distribution_set (id); + + alter table sp_target_info + add constraint fk_targ_stat_targ + foreign key (target_id) + references sp_target (id); + + alter table sp_target_target_tag + add constraint fk_targ_targtag_tag + foreign key (tag) + references sp_target_tag (id); + + alter table sp_target_target_tag + add constraint fk_targ_targtag_target + foreign key (target) + references sp_target (id); + + alter table sp_tenant + add constraint fk_tenant_md_default_ds_type + foreign key (default_ds_type) + references sp_distribution_set_type (id); diff --git a/hawkbit-repository/src/main/resources/db/migration/MYSQL/V1_2_0__update_target_info_for_message___MYSQL.sql b/hawkbit-repository/src/main/resources/db/migration/MYSQL/V1_2_0__update_target_info_for_message___MYSQL.sql new file mode 100755 index 000000000..84f37aed8 --- /dev/null +++ b/hawkbit-repository/src/main/resources/db/migration/MYSQL/V1_2_0__update_target_info_for_message___MYSQL.sql @@ -0,0 +1,8 @@ +DROP INDEX sp_idx_target_info_01 ON sp_target_info; + +ALTER TABLE sp_target_info +CHANGE ip_address address VARCHAR(512); + +UPDATE sp_target_info t1, sp_target_info t2 +SET t1.address = CONCAT('http://',t2.address) +WHERE t1.target_id = t2.target_id AND t2.address is not null \ No newline at end of file diff --git a/hawkbit-repository/src/main/resources/db/migration/MYSQL/V1_4_0__cascade_delete___MYSQL.sql b/hawkbit-repository/src/main/resources/db/migration/MYSQL/V1_4_0__cascade_delete___MYSQL.sql new file mode 100755 index 000000000..a96f76dcb --- /dev/null +++ b/hawkbit-repository/src/main/resources/db/migration/MYSQL/V1_4_0__cascade_delete___MYSQL.sql @@ -0,0 +1,73 @@ +alter table sp_target_info drop FOREIGN KEY fk_targ_stat_targ; +alter table sp_target_info + add constraint fk_targ_stat_targ + foreign key (target_id) + references sp_target (id) + on delete cascade; + +alter table sp_action drop FOREIGN KEY fk_targ_act_hist_targ; +alter table sp_action drop index fk_targ_act_hist_targ; +alter table sp_action + add constraint fk_targ_act_hist_targ + foreign key (target) + references sp_target (id) + on delete cascade; + +alter table sp_action_status drop FOREIGN KEY fk_act_stat_action; +alter table sp_action_status drop index fk_act_stat_action; +alter table sp_action_status + add constraint fk_act_stat_action + foreign key (action) + references sp_action (id) + on delete cascade; + +alter table sp_sw_metadata drop FOREIGN KEY fk_metadata_sw; +alter table sp_sw_metadata drop index fk_metadata_sw; +alter table sp_sw_metadata + add constraint fk_metadata_sw + foreign key (sw_id) + references sp_base_software_module (id) + on delete cascade; + +alter table sp_ds_metadata drop FOREIGN KEY fk_metadata_ds; +alter table sp_ds_metadata + add constraint fk_metadata_ds + foreign key (ds_id) + references sp_distribution_set (id) + on delete cascade; + +alter table sp_action_status_messages drop FOREIGN KEY fk_stat_msg_act_stat; +alter table sp_action_status_messages + add constraint fk_stat_msg_act_stat + foreign key (action_status_id) + references sp_action_status (id) + on delete cascade; + +alter table sp_ds_dstag drop FOREIGN KEY fk_ds_dstag_tag; +alter table sp_ds_dstag drop index fk_ds_dstag_tag; +alter table sp_ds_dstag + add constraint fk_ds_dstag_tag + foreign key (TAG) + references sp_distributionset_tag (id) + on delete cascade; + +alter table sp_ds_dstag drop FOREIGN KEY fk_ds_dstag_ds; +alter table sp_ds_dstag + add constraint fk_ds_dstag_ds + foreign key (ds) + references sp_distribution_set (id) + on delete cascade; + +alter table sp_target_target_tag drop FOREIGN KEY fk_targ_targtag_tag; +alter table sp_target_target_tag + add constraint fk_targ_targtag_tag + foreign key (tag) + references sp_target_tag (id) + on delete cascade; + +alter table sp_target_target_tag drop FOREIGN KEY fk_targ_targtag_target; +alter table sp_target_target_tag + add constraint fk_targ_targtag_target + foreign key (target) + references sp_target (id) + on delete cascade; \ No newline at end of file diff --git a/hawkbit-repository/src/main/resources/db/migration/MYSQL/V1_4_1__cascade_delete___MYSQL.sql b/hawkbit-repository/src/main/resources/db/migration/MYSQL/V1_4_1__cascade_delete___MYSQL.sql new file mode 100755 index 000000000..e1ba9ec74 --- /dev/null +++ b/hawkbit-repository/src/main/resources/db/migration/MYSQL/V1_4_1__cascade_delete___MYSQL.sql @@ -0,0 +1,6 @@ +alter table sp_target_attributes drop FOREIGN KEY fk_targ_attrib_target; +alter table sp_target_attributes + add constraint fk_targ_attrib_target + foreign key (target_id) + references sp_target_info (target_id) + on delete cascade; \ No newline at end of file diff --git a/hawkbit-repository/src/main/resources/db/migration/MYSQL/V1_5_0__target_filter_query___MYSQL.sql b/hawkbit-repository/src/main/resources/db/migration/MYSQL/V1_5_0__target_filter_query___MYSQL.sql new file mode 100755 index 000000000..590329345 --- /dev/null +++ b/hawkbit-repository/src/main/resources/db/migration/MYSQL/V1_5_0__target_filter_query___MYSQL.sql @@ -0,0 +1,15 @@ + create table sp_target_filter_query ( + id bigint not null auto_increment, + created_at bigint, + created_by varchar(40), + last_modified_at bigint, + last_modified_by varchar(40), + optlock_revision bigint, + tenant varchar(40) not null, + name varchar(64) not null, + query varchar(1024) not null, + primary key (id) + ); + + + create index sp_idx_target_filter_query_01 on sp_target_filter_query (tenant, name); diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/AbstractIntegrationTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/AbstractIntegrationTest.java new file mode 100755 index 000000000..cf11fe046 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/AbstractIntegrationTest.java @@ -0,0 +1,344 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import static org.fest.assertions.api.Assertions.assertThat; + +import java.util.List; +import java.util.Properties; +import java.util.concurrent.Callable; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.eclipse.hawkbit.cache.TenantAwareCacheManager; +import org.eclipse.hawkbit.repository.ActionRepository; +import org.eclipse.hawkbit.repository.ActionStatusRepository; +import org.eclipse.hawkbit.repository.ArtifactManagement; +import org.eclipse.hawkbit.repository.ControllerManagement; +import org.eclipse.hawkbit.repository.DeploymentManagement; +import org.eclipse.hawkbit.repository.DistributionSetManagement; +import org.eclipse.hawkbit.repository.DistributionSetRepository; +import org.eclipse.hawkbit.repository.DistributionSetTagRepository; +import org.eclipse.hawkbit.repository.DistributionSetTypeRepository; +import org.eclipse.hawkbit.repository.ExternalArtifactRepository; +import org.eclipse.hawkbit.repository.LocalArtifactRepository; +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.repository.SoftwareModuleMetadataRepository; +import org.eclipse.hawkbit.repository.SoftwareModuleRepository; +import org.eclipse.hawkbit.repository.SoftwareModuleTypeRepository; +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.repository.TagManagement; +import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; +import org.eclipse.hawkbit.repository.TargetInfoRepository; +import org.eclipse.hawkbit.repository.TargetManagement; +import org.eclipse.hawkbit.repository.TargetRepository; +import org.eclipse.hawkbit.repository.TargetTagRepository; +import org.eclipse.hawkbit.repository.TenantMetaDataRepository; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.repository.utils.RepositoryDataGenerator.DatabaseCleanupUtil; +import org.eclipse.hawkbit.security.DosFilter; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.rules.MethodRule; +import org.junit.rules.TestWatchman; +import org.junit.runner.RunWith; +import org.junit.runners.model.FrameworkMethod; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.data.auditing.AuditingHandler; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.gridfs.GridFsOperations; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +@RunWith(SpringJUnit4ClassRunner.class) +@WebAppConfiguration +@SpringApplicationConfiguration(classes = { RepositoryApplicationConfiguration.class, TestConfiguration.class }) +@ActiveProfiles({ "test" }) +@WithUser(principal = "bumlux", allSpPermissions = true, authorities = "ROLE_CONTROLLER") +// destroy the context after each test class because otherwise we get problem +// when context is +// refreshed we e.g. get two instances of CacheManager which leads to very +// strange test failures. +@DirtiesContext(classMode = ClassMode.AFTER_CLASS) +public abstract class AbstractIntegrationTest implements EnvironmentAware { + protected static Logger LOG = null; + + protected final Pageable pageReq = new PageRequest(0, 400); + + @PersistenceContext + protected EntityManager entityManager; + + @Autowired + protected TargetRepository targetRepository; + + @Autowired + protected ActionRepository actionRepository; + + @Autowired + protected DistributionSetRepository distributionSetRepository; + + @Autowired + protected SoftwareModuleRepository softwareModuleRepository; + + @Autowired + protected TenantMetaDataRepository tenantMetaDataRepository; + + @Autowired + protected DistributionSetTypeRepository distributionSetTypeRepository; + + @Autowired + protected SoftwareModuleTypeRepository softwareModuleTypeRepository; + + @Autowired + protected TargetTagRepository targetTagRepository; + + @Autowired + protected DistributionSetTagRepository distributionSetTagRepository; + + @Autowired + protected SoftwareModuleMetadataRepository softwareModuleMetadataRepository; + + @Autowired + protected ActionStatusRepository actionStatusRepository; + + @Autowired + protected ExternalArtifactRepository externalArtifactRepository; + + @Autowired + protected SoftwareManagement softwareManagement; + + @Autowired + protected DistributionSetManagement distributionSetManagement; + + @Autowired + protected ControllerManagement controllerManagement; + + @Autowired + protected TargetManagement targetManagement; + + @Autowired + protected TargetFilterQueryManagement targetFilterQueryManagement; + + @Autowired + protected TagManagement tagManagement; + + @Autowired + protected DeploymentManagement deploymentManagement; + + @Autowired + protected ArtifactManagement artifactManagement; + + @Autowired + protected LocalArtifactRepository artifactRepository; + + @Autowired + protected TargetInfoRepository targetInfoRepository; + + @Autowired + protected GridFsOperations operations; + + @Autowired + protected WebApplicationContext context; + + @Autowired + protected ControllerManagement controllerManagament; + + @Autowired + protected AuditingHandler auditingHandler; + + @Autowired + protected TenantAware tenantAware; + + @Autowired + protected SystemManagement systemManagement; + + @Autowired + protected TenantAwareCacheManager cacheManager; + + protected MockMvc mvc; + + @Autowired + protected DatabaseCleanupUtil dbCleanupUtil; + + protected SoftwareModuleType osType; + protected SoftwareModuleType appType; + protected SoftwareModuleType runtimeType; + + protected DistributionSetType standardDsType; + + @Rule + public final WithSpringAuthorityRule securityRule = new WithSpringAuthorityRule(); + + protected Environment environment = null; + + private static CIMySqlTestDatabase tesdatabase; + + static { + final Properties props = System.getProperties(); + + // if( props.getProperty( SuiteEmbeddedConfiguration.IM_HOME ) == null ) + // { + // props.setProperty( SuiteEmbeddedConfiguration.IM_HOME, + // "./src/test/resources/im" ); + // } + + } + + /* + * (non-Javadoc) + * + * @see org.springframework.context.EnvironmentAware#setEnvironment(org. + * springframework.core.env. Environment) + */ + @Override + public void setEnvironment(final Environment environment) { + this.environment = environment; + } + + @Before + public void before() throws Exception { + mvc = MockMvcBuilders + .webAppContextSetup(context) + .addFilter( + new DosFilter(100, 10, "127\\.0\\.0\\.1|\\[0:0:0:0:0:0:0:1\\]", "(^192\\.168\\.)", + "X-Forwarded-For")) + .addFilter( + new ExcludePathAwareShallowETagFilter( + "/rest/v1/softwaremodules/{smId}/artifacts/{artId}/download", + "/*/controller/artifacts/**")).build(); + + standardDsType = securityRule.runAsPrivileged(new Callable() { + @Override + public DistributionSetType call() throws Exception { + return systemManagement.getTenantMetadata().getDefaultDsType(); + } + }); + + osType = securityRule.runAsPrivileged(new Callable() { + @Override + public SoftwareModuleType call() throws Exception { + return softwareManagement.findSoftwareModuleTypeByKey("os"); + } + }); + + appType = securityRule.runAsPrivileged(new Callable() { + @Override + public SoftwareModuleType call() throws Exception { + return softwareManagement.findSoftwareModuleTypeByKey("application"); + } + }); + runtimeType = securityRule.runAsPrivileged(new Callable() { + @Override + public SoftwareModuleType call() throws Exception { + return softwareManagement.findSoftwareModuleTypeByKey("runtime"); + } + }); + } + + @BeforeClass + public static void beforeClass() { + createTestdatabaseAndStart(); + } + + private static void createTestdatabaseAndStart() { + if ("MYSQL".equals(System.getProperty("spring.jpa.database"))) { + tesdatabase = new CIMySqlTestDatabase(); + tesdatabase.before(); + } + } + + @AfterClass + public static void afterClass() { + if (tesdatabase != null) { + tesdatabase.after(); + } + } + + @After + public void after() throws Exception { + deleteAllRepos(); + cacheManager.getDirectCacheNames().forEach(name -> cacheManager.getDirectCache(name).clear()); + assertThat(actionStatusRepository.findAll()).isEmpty(); + assertThat(targetRepository.findAll()).isEmpty(); + assertThat(actionRepository.findAll()).isEmpty(); + assertThat(distributionSetRepository.findAll()).isEmpty(); + assertThat(targetTagRepository.findAll()).isEmpty(); + assertThat(distributionSetTagRepository.findAll()).isEmpty(); + assertThat(softwareModuleRepository.findAll()).isEmpty(); + assertThat(softwareModuleTypeRepository.findAll()).isEmpty(); + assertThat(distributionSetTypeRepository.findAll()).isEmpty(); + assertThat(tenantMetaDataRepository.findAll()).isEmpty(); + } + + @Transactional + protected void deleteAllRepos() throws Exception { + final List tenants = securityRule.runAs(WithSpringAuthorityRule.withUser(false), + new Callable>() { + @Override + public List call() throws Exception { + return systemManagement.findTenants(); + } + }); + tenants.forEach(tenant -> { + try { + securityRule.runAs(WithSpringAuthorityRule.withUser(false), new Callable() { + @Override + public Void call() throws Exception { + systemManagement.deleteTenant(tenant); + return null; + } + }); + } catch (final Exception e) { + e.printStackTrace(); + } + }); + } + + @Rule + public MethodRule watchman = new TestWatchman() { + @Override + public void starting(final FrameworkMethod method) { + if (LOG != null) { + LOG.info("Starting Test {}...", method.getName()); + } + } + + @Override + public void succeeded(final FrameworkMethod method) { + if (LOG != null) { + LOG.info("Test {} succeeded.", method.getName()); + } + } + + @Override + public void failed(final Throwable e, final FrameworkMethod method) { + if (LOG != null) { + LOG.error("Test {} failed with {}.", method.getName(), e); + } + } + }; +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/AbstractIntegrationTestWithMongoDB.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/AbstractIntegrationTestWithMongoDB.java new file mode 100755 index 000000000..eb2174a57 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/AbstractIntegrationTestWithMongoDB.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.springframework.data.mongodb.core.query.Query; + +import de.flapdoodle.embed.mongo.Command; +import de.flapdoodle.embed.mongo.MongodExecutable; +import de.flapdoodle.embed.mongo.MongodStarter; +import de.flapdoodle.embed.mongo.config.ArtifactStoreBuilder; +import de.flapdoodle.embed.mongo.config.DownloadConfigBuilder; +import de.flapdoodle.embed.mongo.config.IMongodConfig; +import de.flapdoodle.embed.mongo.config.MongodConfigBuilder; +import de.flapdoodle.embed.mongo.config.Net; +import de.flapdoodle.embed.mongo.config.RuntimeConfigBuilder; +import de.flapdoodle.embed.mongo.distribution.Version; +import de.flapdoodle.embed.process.config.store.HttpProxyFactory; +import de.flapdoodle.embed.process.runtime.Network; + +/** + * Test class that contains MonfoDb start and stop for the test + * + * + * + * + */ +public abstract class AbstractIntegrationTestWithMongoDB extends AbstractIntegrationTest { + protected static volatile MongodExecutable mongodExecutable = null; + private static final AtomicInteger mongoLease = new AtomicInteger(0); + private static volatile Integer port; + + @BeforeClass + public static void setupMongo() throws UnknownHostException, IOException { + mongoLease.incrementAndGet(); + if (mongodExecutable == null) { + final Command command = Command.MongoD; + + final RuntimeConfigBuilder runtimeConfig = new RuntimeConfigBuilder().defaults(command); + + if (port == null) { + port = new FreePortFileWriter(28017, 28090, "./target/freeports").getPort(); + System.setProperty("spring.data.mongodb.port", String.valueOf(port)); + } + + Version version = Version.V3_0_5; + if (System.getProperty("inf.mongodb.version") != null) { + version = Version + .valueOf("V" + System.getProperty("inf.mongodb.version").trim().replaceAll("\\.", "_")); + } + + if (System.getProperty("http.proxyHost") != null) { + runtimeConfig + .artifactStore( + new ArtifactStoreBuilder().defaults(command) + .download(new DownloadConfigBuilder().defaultsForCommand(command) + .proxyFactory(new HttpProxyFactory( + System.getProperty("http.proxyHost").trim(), + Integer.valueOf(System.getProperty("http.proxyPort")))))); + } + + final IMongodConfig mongodConfig = new MongodConfigBuilder().version(version) + .net(new Net("127.0.0.1", port, Network.localhostIsIPv6())).build(); + + final MongodStarter starter = MongodStarter.getInstance(runtimeConfig.build()); + mongodExecutable = starter.prepare(mongodConfig); + mongodExecutable.start(); + } + + } + + @After + public void cleanCurrentCollection() { + operations.delete(new Query()); + } + + public void internalShutDownMongo() { + if (mongodExecutable != null && mongoLease.decrementAndGet() <= 0) { + mongodExecutable.stop(); + mongodExecutable = null; + } + } + + @AfterClass + public static void shutdownMongo() throws UnknownHostException, IOException { + if (mongodExecutable != null && mongoLease.decrementAndGet() <= 0) { + mongodExecutable.stop(); + mongodExecutable = null; + } + port = null; + } +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/CIMySqlTestDatabase.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/CIMySqlTestDatabase.java new file mode 100755 index 000000000..0a9b4b1b3 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/CIMySqlTestDatabase.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +import org.apache.commons.lang3.RandomStringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * + */ +public class CIMySqlTestDatabase implements Testdatabase { + + private final static Logger LOG = LoggerFactory.getLogger(CIMySqlTestDatabase.class); + private String schemaName; + private String uri; + private final String username; + private final String password; + + public CIMySqlTestDatabase() { + this.username = System.getProperty("spring.datasource.username"); + this.password = System.getProperty("spring.datasource.password"); + this.uri = System.getProperty("spring.datasource.url"); + createSchemaUri(); + initSystemProperties(); + } + + private final void initSystemProperties() { + System.setProperty("spring.datasource.driverClassName", getDriverClassName()); + System.setProperty("spring.jpa.database", "MYSQL"); + } + + private void createSchemaUri() { + schemaName = "SP" + RandomStringUtils.randomAlphanumeric(10); + this.uri = this.uri.substring(0, uri.lastIndexOf("/") + 1); + + System.setProperty("spring.datasource.url", uri + schemaName); + } + + @Override + public void before() { + createSchema(); + } + + private void createSchema() { + try (Connection connection = DriverManager.getConnection(uri, username, password)) { + connection.prepareStatement("CREATE SCHEMA " + schemaName + ";").execute(); + LOG.info("Schema {} created on uri {}", schemaName, uri); + } catch (final SQLException e) { + e.printStackTrace(); + } + + } + + @Override + public void after() { + dropSchema(); + } + + private void dropSchema() { + try (Connection connection = DriverManager.getConnection(uri, username, password)) { + connection.prepareStatement("DROP SCHEMA " + schemaName + ";").execute(); + LOG.info("Schema {} dropped on uri {}", schemaName, uri); + } catch (final SQLException e) { + e.printStackTrace(); + } + } + + @Override + public String getDriverClassName() { + return "org.mariadb.jdbc.Driver"; + + } + + @Override + public String getUri() { + return uri; + } +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/FreePortFileWriter.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/FreePortFileWriter.java new file mode 100755 index 000000000..c67928910 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/FreePortFileWriter.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import java.io.File; +import java.net.InetSocketAddress; +import java.net.ServerSocket; + +/** + * + * + */ +public class FreePortFileWriter { + + private final String filePortPath; + private final int from; + private final int to; + + /** + * @param from + * @param to + */ + public FreePortFileWriter(final int from, final int to, final String filePortPath) { + this.from = from; + this.to = to; + this.filePortPath = filePortPath; + } + + public int getPort() { + return findFree(); + } + + protected int findFree() { + for (int i = from; i <= to; i++) { + if (isFree(i)) { + return i; + } + } + throw new RuntimeException("No free port in range " + from + ":" + to); + } + + boolean isFree(final int port) { + try { + final File portFile = new File(filePortPath + File.separator + port + ".port"); + portFile.getParentFile().mkdirs(); + if (portFile.exists()) { + return false; + } else { + boolean isFree = false; + final ServerSocket sock = new ServerSocket(); + sock.setReuseAddress(true); + sock.bind(new InetSocketAddress(port)); + if (portFile.createNewFile()) { + portFile.deleteOnExit(); + isFree = true; + } + sock.close(); + // is free: + return isFree; + // We rely on an exception thrown to determine availability or + // not availability. + } + } catch (final Exception e) { + // not free. + return false; + } + } + +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/HashGeneratorUtils.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/HashGeneratorUtils.java new file mode 100755 index 000000000..10f4ede4c --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/HashGeneratorUtils.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Hash utility calls copied from + * http://www.codejava.net/coding/how-to-calculate-md5-and-sha-hash-values-in- + * java. + * + * + * + */ +public final class HashGeneratorUtils { + + private static final Logger LOG = LoggerFactory.getLogger(HashGeneratorUtils.class); + + private HashGeneratorUtils() { + + } + + /** + * Generates a MD5 cryptographic string. + * + * @param message + * the plain message + * @return the cryptographic string + */ + public static String generateMD5(final byte[] message) { + return hashString(message, "MD5"); + } + + /** + * Generates a SHA-1 cryptographic string. + * + * @param message + * the plain message + * @return the cryptographic string + */ + public static String generateSHA1(final byte[] message) { + return hashString(message, "SHA-1"); + } + + /** + * Generates a SHA-256 cryptographic string. + * + * @param message + * the plain message + * @return the cryptographic string + */ + public static String generateSHA256(final byte[] message) { + return hashString(message, "SHA-256"); + } + + private static String hashString(final byte[] message, final String algorithm) { + + try { + final MessageDigest digest = MessageDigest.getInstance(algorithm); + final byte[] hashedBytes = digest.digest(message); + return convertByteArrayToHexString(hashedBytes); + } catch (final NoSuchAlgorithmException e) { + LOG.error("Algorithm could not be find", e); + } + return null; + } + + private static String convertByteArrayToHexString(final byte[] arrayBytes) { + final StringBuilder builder = new StringBuilder(); + for (int i = 0; i < arrayBytes.length; i++) { + builder.append(Integer.toString((arrayBytes[i] & 0xff) + 0x100, 16).substring(1)); + } + return builder.toString(); + } +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/LocalH2TestDatabase.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/LocalH2TestDatabase.java new file mode 100755 index 000000000..a32c481fc --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/LocalH2TestDatabase.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.UUID; + +import org.h2.tools.Server; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * + */ +public class LocalH2TestDatabase implements Testdatabase { + + private final static Logger LOG = LoggerFactory.getLogger(LocalH2TestDatabase.class); + private final int port; + private Server h2server; + private boolean dbStarted; + private String uri; + + public LocalH2TestDatabase(final int port) { + super(); + this.port = port; + createUri(); + initSystemProperties(); + } + + private final void initSystemProperties() { + System.setProperty("spring.datasource.driverClassName", getDriverClassName()); + System.setProperty("spring.datasource.username", ""); + System.setProperty("spring.datasource.password", ""); + System.setProperty("hawkbit.server.database", "H2"); + } + + private void dropAllObjects() { + try (Connection connection = DriverManager.getConnection(uri)) { + connection.prepareCall("DROP ALL OBJECTS;").execute(); + } catch (final SQLException e) { + e.printStackTrace(); + } + } + + @Override + public void before() { + try { + startDatabase(); + } catch (ClassNotFoundException | SQLException | IOException e) { + e.printStackTrace(); + } + } + + @Override + public void after() { + try { + stopDatabase(); + } catch (ClassNotFoundException | SQLException | IOException e) { + e.printStackTrace(); + } + } + + private void startDatabase() throws SQLException, ClassNotFoundException, IOException { + if (dbStarted) { + return; + } + + // Start H2 database for OpenFire + h2server = Server + .createTcpServer( + new String[] { "-tcpPort", String.valueOf(port), "-tcpAllowOthers", "-tcpShutdownForce" }) + .start(); + dbStarted = true; + LOG.info("H2 Database started on port {} and uri {}", port, uri); + dropAllObjects(); + } + + private final void createUri() { + this.uri = "jdbc:h2:tcp://localhost:" + port + "/mem:SP" + UUID.randomUUID().toString() + ";MVCC=TRUE;" + + "DB_CLOSE_DELAY=-1"; + System.setProperty("spring.datasource.url", uri); + } + + private void stopDatabase() throws SQLException, ClassNotFoundException, IOException { + if (!dbStarted) { + return; + } + + h2server.stop(); + h2server = null; + dbStarted = false; + try { + Thread.sleep(1000); + } catch (final InterruptedException e) { + } + } + + @Override + public String getDriverClassName() { + return "org.h2.Driver"; + } + + @Override + public String getUri() { + return uri; + } + +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/MethodSecurityUtil.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/MethodSecurityUtil.java new file mode 100755 index 000000000..2fb68ec31 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/MethodSecurityUtil.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import static org.fest.assertions.Assertions.assertThat; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.security.access.prepost.PreAuthorize; + +public class MethodSecurityUtil { + + private static final Set METHOD_SECURITY_EXCLUSION = new HashSet<>(); + + static { + METHOD_SECURITY_EXCLUSION.add("equals"); + METHOD_SECURITY_EXCLUSION.add("toString"); + METHOD_SECURITY_EXCLUSION.add("hashCode"); + METHOD_SECURITY_EXCLUSION.add("clone"); + METHOD_SECURITY_EXCLUSION.add("setEnvironment"); + // this method shouldn't be public on the DeploymentManagemeht but it is + METHOD_SECURITY_EXCLUSION.add("setOverrideObsoleteUpdateActions"); + METHOD_SECURITY_EXCLUSION.add("isOverrideObsoleteUpdateActions"); + // this method must be public accessible without security because it's + // necessary to acccess + // the security-token of a target without being authenticated because + // the security-token is + // the authentication process + // ControllerManagement#getSecurityTokenByControllerId() + METHOD_SECURITY_EXCLUSION.add("getSecurityTokenByControllerId"); + } + + /** + * asserts that the given methods are annotated with the + * {@link PreAuthorize} annotation for security. Inherited methods are not + * checked. The following methods are excluded due inherited from + * {@link Object}, like equals() or toString(). + * + * @param clazz + * the class to retrieve the public declared methods + */ + public static void assertDeclaredMethodsContainsPreAuthorizeAnnotaions(final Class clazz) { + final Method[] declaredMethods = clazz.getDeclaredMethods(); + for (final Method method : declaredMethods) { + if (!METHOD_SECURITY_EXCLUSION.contains(method.getName()) && !method.isSynthetic() + && Modifier.isPublic(method.getModifiers())) { + final PreAuthorize annotation = method.getAnnotation(PreAuthorize.class); + assertThat(annotation).describedAs( + "The public method " + method.getName() + " is not annoated with @PreAuthorize, security leak?") + .isNotNull(); + } + } + } +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/RandomGeneratedInputStream.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/RandomGeneratedInputStream.java new file mode 100755 index 000000000..bd5be54f5 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/RandomGeneratedInputStream.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import java.io.IOException; +import java.io.InputStream; +import java.security.SecureRandom; +import java.util.Random; + +/** + * + * + * + */ +public class RandomGeneratedInputStream extends InputStream { + + private final Random random = new SecureRandom(); + + /** Target size of the stream. */ + private final long size; + + /** Internal counter. */ + private long index; + + /** + * @param size + * target size of the stream [byte] + */ + public RandomGeneratedInputStream(final long size) { + super(); + + this.size = size; + } + + @Override + public int read() throws IOException { + if (index == size) { + return -1; + } + + index++; + + return random.nextInt(255); + } + +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/TestConfiguration.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/TestConfiguration.java new file mode 100755 index 000000000..7cdca8efd --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/TestConfiguration.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import java.util.concurrent.Executor; + +import org.eclipse.hawkbit.cache.CacheConstants; +import org.eclipse.hawkbit.cache.TenancyCacheManager; +import org.eclipse.hawkbit.cache.TenantAwareCacheManager; +import org.eclipse.hawkbit.repository.utils.RepositoryDataGenerator; +import org.eclipse.hawkbit.repository.utils.RepositoryDataGenerator.DatabaseCleanupUtil; +import org.eclipse.hawkbit.security.SecurityContextTenantAware; +import org.eclipse.hawkbit.security.SecurityProperties; +import org.eclipse.hawkbit.security.SpringSecurityAuditorAware; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.Cache; +import org.springframework.cache.guava.GuavaCacheManager; +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.data.domain.AuditorAware; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; + +import com.google.common.eventbus.AsyncEventBus; +import com.google.common.eventbus.EventBus; +import com.mongodb.MongoClientOptions; + +/** + * Spring context configuration required for Dev.Environment. + * + * + * + */ +@Configuration +@EnableGlobalMethodSecurity(prePostEnabled = true, mode = AdviceMode.ASPECTJ, proxyTargetClass = true, securedEnabled = true) +@EnableConfigurationProperties({ SecurityProperties.class, ControllerPollProperties.class }) +@Profile("test") +public class TestConfiguration implements AsyncConfigurer { + + /** + * DB cleanup utility bean is created. + * + * @return the {@link DatabaseCleanupUtil} bean + */ + @Bean + public DatabaseCleanupUtil createDatabaseCleanupUtil() { + return new RepositoryDataGenerator.DatabaseCleanupUtil(); + } + + @Bean + public MongoClientOptions options() { + return MongoClientOptions.builder().connectTimeout(500).maxWaitTime(500).connectionsPerHost(2) + .serverSelectionTimeout(500).build(); + + } + + @Bean + public TenantAware tenantAware() { + return new SecurityContextTenantAware(); + } + + @Bean + public TenancyCacheManager cacheManager() { + return new TenantAwareCacheManager(new GuavaCacheManager(), tenantAware()); + } + + /** + * Bean for the downlod id cache. + * + * @return the cache + */ + @Bean(name = CacheConstants.DOWNLOAD_ID_CACHE) + public Cache downloadIdCache() { + return cacheManager().getDirectCache(CacheConstants.DOWNLOAD_ID_CACHE); + } + + @Bean + public EventBus eventBus() { + return new AsyncEventBus(asyncExecutor()); + } + + @Bean + public Executor asyncExecutor() { + return new ThreadPoolTaskExecutor(); + } + + @Bean + public AuditorAware auditorAware() { + return new SpringSecurityAuditorAware(); + } + + @Override + public Executor getAsyncExecutor() { + return asyncExecutor(); + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new SimpleAsyncUncaughtExceptionHandler(); + } + +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/TestDataUtil.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/TestDataUtil.java new file mode 100755 index 000000000..befcd6a8a --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/TestDataUtil.java @@ -0,0 +1,366 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Random; +import java.util.UUID; + +import org.apache.commons.io.IOUtils; +import org.eclipse.hawkbit.repository.ArtifactManagement; +import org.eclipse.hawkbit.repository.DistributionSetManagement; +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.repository.TargetManagement; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetTag; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetTag; + +import com.google.common.base.Strings; +import com.google.common.collect.Lists; + +import net._01001111.text.LoremIpsum; + +/** + * Data generator utility for tests. + * + * + * + */ +public class TestDataUtil { + private static final LoremIpsum LOREM = new LoremIpsum(); + + public static List generateDistributionSets(final String suffix, final int number, + final SoftwareManagement softwareManagement, final DistributionSetManagement distributionSetManagement) { + + final List sets = new ArrayList(); + for (int i = 0; i < number; i++) { + sets.add(generateDistributionSet(suffix, "v1." + i, softwareManagement, distributionSetManagement, false)); + } + + return sets; + } + + public static DistributionSet generateDistributionSetWithNoSoftwareModules(final String name, final String version, + final DistributionSetManagement distributionSetManagement) { + + final DistributionSet dis = new DistributionSet(); + dis.setName(name); + dis.setVersion(version); + dis.setDescription("Test describtion for " + name); + return distributionSetManagement.createDistributionSet(dis); + } + + public static List generateDistributionSets(final int number, + final SoftwareManagement softwareManagement, final DistributionSetManagement distributionSetManagement) { + + return generateDistributionSets("", number, softwareManagement, distributionSetManagement); + } + + public static DistributionSet generateDistributionSet(final String suffix, final String version, + final SoftwareManagement softwareManagement, final DistributionSetManagement distributionSetManagement, + final boolean isRequiredMigrationStep) { + + final SoftwareModule ah = softwareManagement.createSoftwareModule(new SoftwareModule( + findOrCreateSoftwareModuleType(softwareManagement, "application"), suffix + "application", + version + "." + new Random().nextInt(100), LOREM.words(20), suffix + " vendor Limited, California")); + final SoftwareModule jvm = softwareManagement + .createSoftwareModule(new SoftwareModule(findOrCreateSoftwareModuleType(softwareManagement, "runtime"), + suffix + "app runtime", version + "." + new Random().nextInt(100), LOREM.words(20), + suffix + " vendor GmbH, Stuttgart, Germany")); + final SoftwareModule os = softwareManagement + .createSoftwareModule(new SoftwareModule(findOrCreateSoftwareModuleType(softwareManagement, "os"), + suffix + " Firmware", version + "." + new Random().nextInt(100), LOREM.words(20), + suffix + " vendor Limited Inc, California")); + + final List mand = new ArrayList<>(); + mand.add(findOrCreateSoftwareModuleType(softwareManagement, "os")); + + final List opt = new ArrayList<>(); + opt.add(findOrCreateSoftwareModuleType(softwareManagement, "application")); + opt.add(findOrCreateSoftwareModuleType(softwareManagement, "runtime")); + + return distributionSetManagement.createDistributionSet( + buildDistributionSet(suffix != null && suffix.length() > 0 ? suffix : "DS", version, + findOrCreateDistributionSetType(distributionSetManagement, "ecl_os_app_jvm", + "OC mandatory App/JVM optional", mand, opt), + os, jvm, ah).setRequiredMigrationStep(isRequiredMigrationStep)); + } + + public static DistributionSet generateDistributionSet(final String suffix, final String version, + final SoftwareManagement softwareManagement, final DistributionSetManagement distributionSetManagement, + final Collection tags) { + + final DistributionSet set = generateDistributionSet(suffix, version, softwareManagement, + distributionSetManagement, false); + + final List sets = new ArrayList(); + sets.add(set); + + tags.forEach(tag -> distributionSetManagement.toggleTagAssignment(sets, tag)); + + return distributionSetManagement.findDistributionSetById(set.getId()); + + } + + public static List generateTargets(final int number) { + return generateTargets(0, number, "Test target "); + } + + public static List generateTargets(final int number, final String prefix) { + return generateTargets(0, number, prefix); + } + + public static List generateTargets(final int start, final int number, final String prefix) { + final List targets = new ArrayList<>(); + for (int i = start; i < start + number; i++) { + targets.add(new Target(prefix + i)); + } + + return targets; + } + + public static List generateTargetTags(final int number) { + final List result = new ArrayList<>(); + + for (int i = 0; i < number; i++) { + result.add(new TargetTag("tag" + i, "tagdesc" + i, "" + i)); + } + + return result; + } + + public static List generateDistributionSetTags(final int number) { + final List result = new ArrayList(); + + for (int i = 0; i < number; i++) { + result.add(new DistributionSetTag("tag" + i, "tagdesc" + i, "" + i)); + } + + return result; + } + + public static DistributionSet generateDistributionSet(final String suffix, + final SoftwareManagement softwareManagement, final DistributionSetManagement distributionSetManagement, + final boolean isRequiredMigrationStep) { + return generateDistributionSet(suffix, "v1.0", softwareManagement, distributionSetManagement, + isRequiredMigrationStep); + } + + public static DistributionSet generateDistributionSet(final String suffix, + final SoftwareManagement softwareManagement, final DistributionSetManagement distributionSetManagement) { + return generateDistributionSet(suffix, "v1.0", softwareManagement, distributionSetManagement, false); + } + + public static List generateArtifacts( + final ArtifactManagement artifactManagement, final Long moduleId) { + final List artifacts = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + final InputStream stubInputStream = IOUtils.toInputStream("some test data" + i); + artifacts.add(artifactManagement.createLocalArtifact(stubInputStream, moduleId, "filename" + i, false)); + + } + + return artifacts; + } + + public static Target createTarget(final TargetManagement targetManagement) { + final String targetExist = "targetExist"; + final Target target = new Target(targetExist); + targetManagement.createTarget(target); + return target; + } + + public static DistributionSet generateDistributionSet(final String suffix, + final SoftwareManagement softwareManagement, final DistributionSetManagement distributionSetManagement, + final Collection tags) { + return generateDistributionSet(suffix, "v1.0", softwareManagement, distributionSetManagement, tags); + } + + public static SoftwareModuleType findOrCreateSoftwareModuleType(final SoftwareManagement softwareManagement, + final String softwareModuleType) { + final SoftwareModuleType findSoftwareModuleTypeByKey = softwareManagement + .findSoftwareModuleTypeByKey(softwareModuleType); + if (findSoftwareModuleTypeByKey != null) { + return findSoftwareModuleTypeByKey; + } + return softwareManagement.createSoftwareModuleType(new SoftwareModuleType(softwareModuleType, + softwareModuleType, "Standard type " + softwareManagement, 1)); + } + + public static DistributionSetType findOrCreateDistributionSetType( + final DistributionSetManagement distributionSetManagement, final String dsTypeKey, final String dsTypeName, + final Collection mandatory, final Collection optional) { + final DistributionSetType findDistributionSetTypeByname = distributionSetManagement + .findDistributionSetTypeByKey(dsTypeKey); + + if (findDistributionSetTypeByname != null) { + return findDistributionSetTypeByname; + } + + final DistributionSetType type = new DistributionSetType(dsTypeKey, dsTypeName, "Standard type" + dsTypeName); + mandatory.forEach(entry -> type.addMandatoryModuleType(entry)); + optional.forEach(entry -> type.addOptionalModuleType(entry)); + + return distributionSetManagement.createDistributionSetType(type); + } + + /** + * builds a set of {@link Target} fixtures from the given parameters. + * + * @param noOfTgts + * number of targets to create + * @param ctlrIDPrefix + * prefix used for the controller ID + * @param descriptionPrefix + * prefix used for the description + * @return set of {@link Target} + */ + public static List buildTargetFixtures(final int noOfTgts, final String ctlrIDPrefix, + final String descriptionPrefix) { + return buildTargetFixtures(noOfTgts, ctlrIDPrefix, descriptionPrefix, null); + } + + /** + * method creates set of targets by by generating the controller ID and the + * description like: prefix + no of target. + * + * @param noOfTgts + * number of targets which should be created + * @param ctlrIDPrefix + * prefix of the controllerID which is concatenated with the + * number of the target + * @param descriptionPrefix + * prefix of the target description which is concatenated with + * the number of the target + * @param tags + * tags which should be added to the created {@link Target}s + * @return set of created targets + */ + public static List buildTargetFixtures(final int noOfTgts, final String ctlrIDPrefix, + final String descriptionPrefix, final TargetTag[] tags) { + final List list = new ArrayList(); + for (int i = 0; i < noOfTgts; i++) { + String ctrlID = ctlrIDPrefix; + if (Strings.isNullOrEmpty(ctrlID)) { + ctrlID = UUID.randomUUID().toString(); + } + ctrlID = String.format("%s-%05d", ctrlID, i); + + final String description = String.format("the description of ProvisioningTarget: [%s]", ctrlID); + + final Target target = buildTargetFixture(ctrlID, description, tags); + list.add(target); + + } + return list; + } + + /** + * builds a single {@link Target} fixture from the given parameters. + * + * @param ctrlID + * controllerID + * @param description + * the description of the target + * @param tags + * assigned {@link TargetTag}s + * @return the created {@link Target} + */ + public static Target buildTargetFixture(final String ctrlID, final String description, final TargetTag[] tags) { + final Target target = new Target(ctrlID); + target.setName("Prov.Target ".concat(ctrlID)); + target.setDescription(description); + if (tags != null && tags.length > 0) { + for (final TargetTag t : tags) { + target.getTags().add(t); + } + } + return target; + } + + /** + * builder method for creating a single target object. + * + * @param ctrlID + * the ID of the target + * @param description + * of the target + * @return the created target object + */ + public static Target buildTargetFixture(final String ctrlID, final String description) { + return buildTargetFixture(ctrlID, description, null); + } + + /** + * builder method for creating a {@link DistributionSet}. + * + * @param name + * of the DS + * @param version + * of the DS + * @param os + * operating system of the DS + * @param jvm + * java virtual machine of the DS + * @param agentHub + * of the DS + * @return the created {@link DistributionSet} + */ + public static DistributionSet buildDistributionSet(final String name, final String version, + final DistributionSetType type, final SoftwareModule os, final SoftwareModule jvm, + final SoftwareModule agentHub) { + final DistributionSet distributionSet = new DistributionSet(name, version, null, type, + Lists.newArrayList(os, jvm, agentHub)); + distributionSet.setDescription( + String.format("description of DistributionSet; name = '%s', version = '%s'", name, version)); + return distributionSet; + } + + /** + * builder method for creating a set of {@link TargetTag}. + * + * @param noOfTags + * number of {@link TargetTag}. to be created + * @param tagPrefix + * prefix for the {@link TargetTag.getName()} + * @return the created set of {@link TargetTag}s + */ + public static List buildTargetTagFixtures(final int noOfTags, final String tagPrefix) { + final List list = new ArrayList<>(); + for (int i = 0; i < noOfTags; i++) { + String tagName = "myTag"; + if (!Strings.isNullOrEmpty(tagPrefix)) { + tagName = tagPrefix; + } + tagName = String.format("%s-%05d", tagName, i); + + final TargetTag targetTag = buildTargetTagFixture(tagName); + list.add(targetTag); + } + return list; + } + + /** + * builder method for creating a simple {@link TargetTag}. + * + * @param tagName + * name of the Tag + * @return the {@link TargetTag} + */ + public static TargetTag buildTargetTagFixture(final String tagName) { + return new TargetTag(tagName); + } +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/Testdatabase.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/Testdatabase.java new file mode 100755 index 000000000..a11984481 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/Testdatabase.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +/** + * + * + */ +public interface Testdatabase { + + void before(); + + void after(); + + public String getUri(); + + String getDriverClassName(); + +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/WithSpringAuthorityRule.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/WithSpringAuthorityRule.java new file mode 100755 index 000000000..15fda51b4 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/WithSpringAuthorityRule.java @@ -0,0 +1,274 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; + +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails; +import org.eclipse.hawkbit.repository.model.helper.SystemManagementHolder; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +public class WithSpringAuthorityRule implements TestRule { + + /* + * (non-Javadoc) + * + * @see org.junit.rules.TestRule#apply(org.junit.runners.model.Statement, + * org.junit.runner.Description) + */ + @Override + public Statement apply(final Statement base, final Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + final SecurityContext oldContext = before(description); + try { + base.evaluate(); + } finally { + after(oldContext); + } + } + }; + } + + private SecurityContext before(final Description description) throws Throwable { + final SecurityContext oldContext = SecurityContextHolder.getContext(); + WithUser annotation = description.getAnnotation(WithUser.class); + if (annotation == null) { + annotation = description.getTestClass().getAnnotation(WithUser.class); + } + if (annotation != null) { + setSecurityContext(annotation); + if (annotation.autoCreateTenant()) { + SystemManagementHolder.getInstance().getSystemManagement().getTenantMetadata(annotation.tenantId()); + } + } + return oldContext; + } + + /** + * @param annotation + */ + private void setSecurityContext(final WithUser annotation) { + SecurityContextHolder.setContext(new SecurityContext() { + /** + * + */ + private static final long serialVersionUID = 1L; + + @Override + public void setAuthentication(final Authentication authentication) { + } + + @Override + public Authentication getAuthentication() { + final String[] authorities; + if (annotation.allSpPermissions()) { + authorities = getAllAuthorities(annotation.authorities(), annotation.removeFromAllPermission()); + } else { + authorities = annotation.authorities(); + } + final TestingAuthenticationToken testingAuthenticationToken = new TestingAuthenticationToken( + annotation.principal(), annotation.credentials(), authorities); + testingAuthenticationToken + .setDetails(new TenantAwareAuthenticationDetails(annotation.tenantId(), false)); + return testingAuthenticationToken; + } + + private String[] getAllAuthorities(final String[] additionalAuthorities, final String[] notInclude) { + final List allPermissions = new ArrayList<>(); + final Field[] declaredFields = SpPermission.class.getDeclaredFields(); + for (final Field field : declaredFields) { + if (Modifier.isPublic(field.getModifiers()) && Modifier.isStatic(field.getModifiers())) { + field.setAccessible(true); + try { + boolean addPermission = true; + final String permissionName = (String) field.get(null); + if (notInclude != null) { + for (final String notInlcudePerm : notInclude) { + if (permissionName.equals(notInlcudePerm)) { + addPermission = false; + break; + } + } + } + if (addPermission) { + allPermissions.add(permissionName); + } + } catch (IllegalArgumentException | IllegalAccessException e) { + // nope + } + } + } + for (final String authority : additionalAuthorities) { + allPermissions.add(authority); + } + return allPermissions.toArray(new String[allPermissions.size()]); + } + }); + } + + private void after(final SecurityContext oldContext) { + SecurityContextHolder.setContext(oldContext); + } + + /** + * @param callable + * @return + * @throws Exception + */ + public T runAsPrivileged(final Callable callable) throws Exception { + return runAs(privilegedUser(), callable); + } + + /** + * + * @param withUser + * @param callable + * @return + * @throws Exception + */ + public T runAs(final WithUser withUser, final Callable callable) throws Exception { + final SecurityContext oldContext = SecurityContextHolder.getContext(); + setSecurityContext(withUser); + if (withUser.autoCreateTenant()) { + SystemManagementHolder.getInstance().getSystemManagement().getTenantMetadata(withUser.tenantId()); + } + try { + return callable.call(); + } finally { + after(oldContext); + } + } + + public static WithUser withUser(final String principal, final String... authorities) { + return withUserAndTenant(principal, "default", true, authorities); + } + + public static WithUser withUser(final boolean autoCreateTenant) { + return withUserAndTenant("bumlux", "default", autoCreateTenant, new String[] {}); + } + + public static WithUser withUserAndTenant(final String principal, final String tenant, final String... authorities) { + return withUserAndTenant(principal, tenant, true, new String[] {}); + } + + public static WithUser withUserAndTenant(final String principal, final String tenant, + final boolean autoCreateTenant, final String... authorities) { + return new WithUser() { + + @Override + public Class annotationType() { + return WithUser.class; + } + + @Override + public String principal() { + return principal; + } + + @Override + public String credentials() { + return null; + } + + @Override + public String[] authorities() { + return authorities; + } + + @Override + public boolean allSpPermissions() { + return true; + } + + @Override + public String[] removeFromAllPermission() { + return null; + } + + @Override + public String tenantId() { + return tenant; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.WithUser#autoCreateTenant() + */ + @Override + public boolean autoCreateTenant() { + return autoCreateTenant; + } + }; + } + + private static WithUser privilegedUser() { + return new WithUser() { + + @Override + public Class annotationType() { + return WithUser.class; + } + + @Override + public String principal() { + return "bumlux"; + } + + @Override + public String credentials() { + return null; + } + + @Override + public String[] authorities() { + return new String[] { "ROLE_CONTROLLER" }; + } + + @Override + public boolean allSpPermissions() { + return true; + } + + @Override + public String[] removeFromAllPermission() { + return null; + } + + @Override + public String tenantId() { + return "default"; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.WithUser#autoCreateTenant() + */ + @Override + public boolean autoCreateTenant() { + return true; + } + }; + } +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/WithUser.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/WithUser.java new file mode 100755 index 000000000..3b0412ff1 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/WithUser.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to run test classes or test methods with a specific user with + * specific permissions. + * + * + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Inherited +public @interface WithUser { + + /** + * Gets the test principal. + * + * @return test principal + */ + String principal() default "TestPrincipal"; + + /** + * Gets the test credentials. + * + * @return test credentials + */ + String credentials() default "TestCredentials"; + + /** + * Gets the test tenant id. + * + * @return test tenant id + */ + String tenantId() default "default"; + + /** + * Should tenant auto created. + * + * @return = auto create not create + */ + boolean autoCreateTenant() default true; + + /** + * Gets the test authorities. + * + * @return authorities + */ + String[] authorities() default {}; + + /** + * Gets the test all permissions. + * + * @return permissions + */ + boolean allSpPermissions() default false; + + /** + * Gets the test removeFromAllPermission. + * + * @return removeFromAllPermission + */ + String[] removeFromAllPermission() default {}; +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/cache/CacheKeysTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/cache/CacheKeysTest.java new file mode 100755 index 000000000..b086875c3 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/cache/CacheKeysTest.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.cache; + +import static org.fest.assertions.api.Assertions.assertThat; + +import org.junit.Test; + +public class CacheKeysTest { + + @Test + public void entitySpecificCacheKeyPattern() { + final String knownEntityId = "123"; + final String knownCacheKey = "someField"; + final String entitySpecificCacheKey = CacheKeys.entitySpecificCacheKey(knownEntityId, knownCacheKey); + assertThat(entitySpecificCacheKey).isEqualTo(knownEntityId + "." + knownCacheKey); + } + +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/cache/CacheWriteNotifyTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/cache/CacheWriteNotifyTest.java new file mode 100755 index 000000000..3b69b4fed --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/cache/CacheWriteNotifyTest.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.cache; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.eclipse.hawkbit.eventbus.event.DownloadProgressEvent; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +import com.google.common.eventbus.EventBus; + +@RunWith(MockitoJUnitRunner.class) +public class CacheWriteNotifyTest { + + @Mock + private EventBus eventBusMock; + + @Mock + private CacheManager cacheManagerMock; + + @Mock + private Cache cacheMock; + + @Mock + private TenantAware tenantAwareMock; + + private CacheWriteNotify underTest; + + @Before + public void before() { + underTest = new CacheWriteNotify(); + underTest.setEventBus(eventBusMock); + underTest.setCacheManager(cacheManagerMock); + underTest.setTenantAware(tenantAwareMock); + } + + @Test + public void downloadgProgressIsCachedAndEventSent() { + final long knownStatusId = 1; + final int knownPercentage = 23; + + when(cacheManagerMock.getCache(Action.class.getName())).thenReturn(cacheMock); + when(tenantAwareMock.getCurrentTenant()).thenReturn("default"); + + underTest.downloadProgressPercent(knownStatusId, knownPercentage); + + verify(cacheManagerMock).getCache(eq(Action.class.getName())); + verify(cacheMock).put(knownStatusId + "." + CacheKeys.DOWNLOAD_PROGRESS_PERCENT, knownPercentage); + verify(eventBusMock).post(any(DownloadProgressEvent.class)); + } + +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/eventbus/CacheFieldEntityListenerTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/eventbus/CacheFieldEntityListenerTest.java new file mode 100755 index 000000000..e80c49b2d --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/eventbus/CacheFieldEntityListenerTest.java @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.eventbus; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.eclipse.hawkbit.cache.CacheField; +import org.eclipse.hawkbit.cache.CacheKeys; +import org.eclipse.hawkbit.repository.model.helper.CacheManagerHolder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.support.SimpleValueWrapper; +import org.springframework.hateoas.Identifiable; + +@RunWith(MockitoJUnitRunner.class) +public class CacheFieldEntityListenerTest { + + private static final String TEST_CACHE_FIELD = "testCacheField"; + + @Mock + private CacheManager cacheManagerMock; + + @Mock + private Cache cacheMock; + + private CacheFieldEntityListener underTest; + + @Before + public void before() { + // mock + when(cacheManagerMock.getCache(anyString())).thenReturn(cacheMock); + + underTest = new CacheFieldEntityListener(); + CacheManagerHolder.getInstance().setCacheManager(cacheManagerMock); + } + + @Test + public void postLoadSetsCacheFields() { + final String entityId = "123"; + final String normalFieldValue = "bumlux"; + final TestEntity testObject = new TestEntity(entityId, normalFieldValue); + final int expectedTestValue = -1; + + when(cacheMock.get(CacheKeys.entitySpecificCacheKey(entityId, TEST_CACHE_FIELD))) + .thenReturn(new SimpleValueWrapper(expectedTestValue)); + + // pre verify everything is ok + assertThat(testObject.getCacheField()).isNotEqualTo(expectedTestValue); + assertThat(testObject.getNormalField()).isEqualTo(normalFieldValue); + + // test + underTest.postLoad(testObject); + + assertThat(testObject.getNormalField()).isEqualTo(normalFieldValue); + // now cache value should be like the value in the mock of the cache + assertThat(testObject.getCacheField()).isEqualTo(expectedTestValue); + } + + @Test + public void postRemoveEvictsCacheField() { + final String entityId = "123"; + final String normalFieldValue = "bumlux"; + final TestEntity testObject = new TestEntity(entityId, normalFieldValue); + // test + underTest.postDelete(testObject); + + verify(cacheMock).evict(eq(CacheKeys.entitySpecificCacheKey(entityId, TEST_CACHE_FIELD))); + } + + private final class TestEntity implements Identifiable { + + private final String id; + + private final String normalField; + + @CacheField(key = TEST_CACHE_FIELD) + private int cacheField; + + private TestEntity(final String id, final String normalFieldValue) { + this.id = id; + this.normalField = normalFieldValue; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.hateoas.Identifiable#getId() + */ + @Override + public String getId() { + return id; + } + + /** + * @return the normalField + */ + String getNormalField() { + return normalField; + } + + /** + * @return the cacheField + */ + int getCacheField() { + return cacheField; + } + } +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/ActionTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/ActionTest.java new file mode 100755 index 000000000..0ce7dd677 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/ActionTest.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import static org.fest.assertions.api.Assertions.assertThat; + +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.junit.Test; + +public class ActionTest { + + // issue MECS-670 timeforced update and eTAG calculation + @Test + public void timeforcedHitNewHasCodeIsGenerated() throws InterruptedException { + + final boolean active = true; + // current time + 1 seconds + final long sleepTime = 1000; + final long timeForceTimeAt = System.currentTimeMillis() + sleepTime; + final Action timeforcedAction = new Action(); + timeforcedAction.setActionType(ActionType.TIMEFORCED); + timeforcedAction.setForcedTime(timeForceTimeAt); + final int knownHashCode = timeforcedAction.hashCode(); + assertThat(timeforcedAction.isForce()).isFalse(); + + // wait until timeforce time is hit + Thread.sleep(sleepTime + 100); + assertThat(timeforcedAction.isForce()).isTrue(); + assertThat(timeforcedAction.hashCode()).isNotEqualTo(knownHashCode); + } +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/ArtifactManagementNoMongoDbTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/ArtifactManagementNoMongoDbTest.java new file mode 100755 index 000000000..e509a3b0c --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/ArtifactManagementNoMongoDbTest.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import org.apache.commons.lang3.RandomStringUtils; +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.repository.exception.ArtifactUploadFailedException; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.junit.BeforeClass; +import org.junit.Test; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * Addition tests next to {@link ArtifactManagementTest} with no running MongoDB + * + * + * + * + */ +@Features("Component Tests - Repository") +@Stories("Artifact Management") +public class ArtifactManagementNoMongoDbTest extends AbstractIntegrationTest { + + @BeforeClass + public static void initialize() { + // set property to mongoPort which does not start any mongoDB of + // parallel test execution + System.setProperty("spring.data.mongodb.port", "1020"); + } + + @Test(expected = ArtifactUploadFailedException.class) + @Description("Checks if the expected ArtifactUploadFailedException is thrown in case of MongoDB down") + public void createLocalArtifactWithMongoDbDown() throws IOException { + SoftwareModule sm = new SoftwareModule(softwareManagement.findSoftwareModuleTypeByKey("os"), "name 1", + "version 1", null, null); + sm = softwareModuleRepository.save(sm); + + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + + artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), sm.getId(), "file1", false); + } + +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/ArtifactManagementTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/ArtifactManagementTest.java new file mode 100755 index 000000000..5585aab95 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/ArtifactManagementTest.java @@ -0,0 +1,393 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.UnknownHostException; +import java.security.NoSuchAlgorithmException; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.eclipse.hawkbit.AbstractIntegrationTestWithMongoDB; +import org.eclipse.hawkbit.HashGeneratorUtils; +import org.eclipse.hawkbit.RandomGeneratedInputStream; +import org.eclipse.hawkbit.WithUser; +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.repository.exception.ArtifactDeleteFailedException; +import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException; +import org.eclipse.hawkbit.repository.model.Artifact; +import org.eclipse.hawkbit.repository.model.ExternalArtifact; +import org.eclipse.hawkbit.repository.model.ExternalArtifactProvider; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.junit.Test; +import org.slf4j.LoggerFactory; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * Test class for {@link ArtifactManagement} with running MongoDB instance.. + * + * + * + * + */ +@Features("Component Tests - Repository") +@Stories("Artifact Management") +public class ArtifactManagementTest extends AbstractIntegrationTestWithMongoDB { + public ArtifactManagementTest() { + LOG = LoggerFactory.getLogger(ArtifactManagementTest.class); + } + + /** + * Test method for + * {@link org.eclipse.hawkbit.repository.ArtifactManagement#createLocalArtifact(java.io.InputStream)} + * . + * + * @throws IOException + * @throws NoSuchAlgorithmException + */ + @Test + @Description("Test if a local artifact can be created by API including metadata.") + public void createLocalArtifact() throws NoSuchAlgorithmException, IOException { + // checkbaseline + assertThat(softwareModuleRepository.findAll()).hasSize(0); + assertThat(artifactRepository.findAll()).hasSize(0); + + SoftwareModule sm = new SoftwareModule(softwareManagement.findSoftwareModuleTypeByKey("os"), "name 1", + "version 1", null, null); + sm = softwareModuleRepository.save(sm); + + SoftwareModule sm2 = new SoftwareModule(softwareManagement.findSoftwareModuleTypeByKey("os"), "name 2", + "version 2", null, null); + sm2 = softwareModuleRepository.save(sm2); + + SoftwareModule sm3 = new SoftwareModule(softwareManagement.findSoftwareModuleTypeByKey("os"), "name 3", + "version 3", null, null); + sm3 = softwareModuleRepository.save(sm3); + + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + + final Artifact result = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), sm.getId(), + "file1", false); + final Artifact result11 = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), sm.getId(), + "file11", false); + final Artifact result12 = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), sm.getId(), + "file12", false); + final Artifact result2 = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), sm2.getId(), + "file2", false); + + assertThat(result).isInstanceOf(LocalArtifact.class); + assertThat(result.getSoftwareModule().getId()).isEqualTo(sm.getId()); + assertThat(result2.getSoftwareModule().getId()).isEqualTo(sm2.getId()); + assertThat(((LocalArtifact) result).getFilename()).isEqualTo("file1"); + assertThat(((LocalArtifact) result).getGridFsFileName()).isNotNull(); + assertThat(result).isNotEqualTo(result2); + assertThat(((LocalArtifact) result).getGridFsFileName()) + .isEqualTo(((LocalArtifact) result2).getGridFsFileName()); + + assertThat(artifactManagement.findLocalArtifactByFilename("file1").get(0).getSha1Hash()) + .isEqualTo(HashGeneratorUtils.generateSHA1(random)); + assertThat(artifactManagement.findLocalArtifactByFilename("file1").get(0).getMd5Hash()) + .isEqualTo(HashGeneratorUtils.generateMD5(random)); + + assertThat(artifactRepository.findAll()).hasSize(4); + assertThat(softwareModuleRepository.findAll()).hasSize(3); + + assertThat(softwareManagement.findSoftwareModuleWithDetails(sm.getId()).getArtifacts()).hasSize(3); + } + + @Test + @Description("Tests hard delete directly on repository.") + public void hardDeleteSoftwareModule() throws NoSuchAlgorithmException, IOException { + SoftwareModule sm = new SoftwareModule(softwareManagement.findSoftwareModuleTypeByKey("os"), "name 1", + "version 1", null, null); + sm = softwareModuleRepository.save(sm); + + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + + artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), sm.getId(), "file1", false); + assertThat(artifactRepository.findAll()).hasSize(1); + + softwareModuleRepository.deleteAll(); + assertThat(artifactRepository.findAll()).hasSize(0); + } + + /** + * Test method for + * {@link org.eclipse.hawkbit.repository.ArtifactManagement#createExternalArtifact(org.eclipse.hawkbit.repository.model.ExternalArtifactProvider, java.lang.String)} + * . + */ + @Test + @Description("Tests the creation of an external artifact metadata element.") + public void createExternalArtifact() { + SoftwareModule sm = new SoftwareModule(softwareManagement.findSoftwareModuleTypeByKey("os"), "name 1", + "version 1", null, null); + sm = softwareModuleRepository.save(sm); + + SoftwareModule sm2 = new SoftwareModule(softwareManagement.findSoftwareModuleTypeByKey("os"), "name 2", + "version 2", null, null); + sm2 = softwareModuleRepository.save(sm2); + + final ExternalArtifactProvider provider = artifactManagement.createExternalArtifactProvider("provider X", null, + "https://fhghdfjgh", "/{version}/"); + + ExternalArtifact result = artifactManagement.createExternalArtifact(provider, null, sm.getId()); + + assertNotNull(result); + assertThat(externalArtifactRepository.findAll()).contains(result).hasSize(1); + assertThat(result.getSoftwareModule().getId()).isEqualTo(sm.getId()); + assertThat(result.getUrl()).isEqualTo("https://fhghdfjgh/{version}/"); + assertThat(result.getExternalArtifactProvider()).isEqualTo(provider); + + result = artifactManagement.createExternalArtifact(provider, "/test", sm2.getId()); + assertNotNull(result); + assertThat(externalArtifactRepository.findAll()).contains(result).hasSize(2); + assertThat(result.getUrl()).isEqualTo("https://fhghdfjgh/test"); + assertThat(result.getExternalArtifactProvider()).isEqualTo(provider); + } + + @Test + @Description("Tests deletio of an external artifact metadata element.") + public void deleteExternalArtifact() { + assertThat(artifactRepository.findAll()).isEmpty(); + + SoftwareModule sm = new SoftwareModule(softwareManagement.findSoftwareModuleTypeByKey("os"), "name 1", + "version 1", null, null); + sm = softwareModuleRepository.save(sm); + + final ExternalArtifactProvider provider = artifactManagement.createExternalArtifactProvider("provider X", null, + "https://fhghdfjgh", "/{version}/"); + + final ExternalArtifact result = artifactManagement.createExternalArtifact(provider, null, sm.getId()); + assertNotNull(result); + assertThat(externalArtifactRepository.findAll()).contains(result).hasSize(1); + + artifactManagement.deleteExternalArtifact(result.getId()); + assertThat(externalArtifactRepository.findAll()).isEmpty(); + } + + /** + * Test method for + * {@link org.eclipse.hawkbit.repository.ArtifactManagement#deleteLocalArtifact(java.lang.Long)} + * . + * + * @throws IOException + * @throws NoSuchAlgorithmException + */ + @Test + @Description("Tests the deletion of a local artifact including metadata.") + public void deleteLocalArtifact() throws NoSuchAlgorithmException, IOException { + SoftwareModule sm = new SoftwareModule(softwareManagement.findSoftwareModuleTypeByKey("os"), "name 1", + "version 1", null, null); + sm = softwareModuleRepository.save(sm); + + SoftwareModule sm2 = new SoftwareModule(softwareManagement.findSoftwareModuleTypeByKey("os"), "name 2", + "version 2", null, null); + sm2 = softwareModuleRepository.save(sm2); + + assertThat(artifactRepository.findAll()).isEmpty(); + + final Artifact result = artifactManagement.createLocalArtifact(new RandomGeneratedInputStream(5 * 1024), + sm.getId(), "file1", false); + final Artifact result2 = artifactManagement.createLocalArtifact(new RandomGeneratedInputStream(5 * 1024), + sm2.getId(), "file2", false); + + assertThat(artifactRepository.findAll()).hasSize(2); + + assertThat(result.getId()).isNotNull(); + assertThat(result2.getId()).isNotNull(); + assertThat(((LocalArtifact) result).getGridFsFileName()) + .isNotEqualTo(((LocalArtifact) result2).getGridFsFileName()); + assertThat(operations.findOne( + new Query().addCriteria(Criteria.where("filename").is(((LocalArtifact) result).getGridFsFileName())))) + .isNotNull(); + assertThat(operations.findOne( + new Query().addCriteria(Criteria.where("filename").is(((LocalArtifact) result2).getGridFsFileName())))) + .isNotNull(); + + artifactManagement.deleteLocalArtifact(result.getId()); + assertThat(operations.findOne( + new Query().addCriteria(Criteria.where("filename").is(((LocalArtifact) result).getGridFsFileName())))) + .isNull(); + assertThat(operations.findOne( + new Query().addCriteria(Criteria.where("filename").is(((LocalArtifact) result2).getGridFsFileName())))) + .isNotNull(); + + artifactManagement.deleteLocalArtifact(result2.getId()); + assertThat(operations.findOne( + new Query().addCriteria(Criteria.where("filename").is(((LocalArtifact) result2).getGridFsFileName())))) + .isNull(); + + assertThat(artifactRepository.findAll()).hasSize(0); + } + + @Test + @Description("Trys and fails to delete local artifact with a down mongodb and checks if expected ArtifactDeleteFailedException is thrown.") + public void deleteArtifactsWithNoMongoDb() throws UnknownHostException, IOException { + // ensure baseline + assertThat(artifactRepository.findAll()).isEmpty(); + + // prepare test + SoftwareModule sm = new SoftwareModule(softwareManagement.findSoftwareModuleTypeByKey("os"), "name 1", + "version 1", null, null); + sm = softwareModuleRepository.save(sm); + + final Artifact result = artifactManagement.createLocalArtifact(new RandomGeneratedInputStream(5 * 1024), + sm.getId(), "file1", false); + + assertThat(artifactRepository.findAll()).hasSize(1); + + internalShutDownMongo(); + try { + artifactManagement.deleteLocalArtifact(result.getId()); + fail("deletion should have failed"); + } catch (final ArtifactDeleteFailedException e) { + + } + setupMongo(); + + assertThat(artifactRepository.findAll()).hasSize(1); + assertThat(artifactManagement.findArtifact(result.getId())).isEqualTo(result); + + } + + @Test + @Description("Test the deletion of an artifact metadata where the binary is still linked to another " + + "metadata element. The expected result is that the metadata is deleted but the binary kept.") + public void deleteDuplicateArtifacts() throws NoSuchAlgorithmException, IOException { + SoftwareModule sm = new SoftwareModule(softwareManagement.findSoftwareModuleTypeByKey("os"), "name 1", + "version 1", null, null); + sm = softwareModuleRepository.save(sm); + + SoftwareModule sm2 = new SoftwareModule(softwareManagement.findSoftwareModuleTypeByKey("os"), "name 2", + "version 2", null, null); + sm2 = softwareModuleRepository.save(sm2); + + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + + final Artifact result = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), sm.getId(), + "file1", false); + final Artifact result2 = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), sm2.getId(), + "file2", false); + + assertThat(artifactRepository.findAll()).hasSize(2); + assertThat(result.getId()).isNotNull(); + assertThat(result2.getId()).isNotNull(); + assertThat(((LocalArtifact) result).getGridFsFileName()) + .isEqualTo(((LocalArtifact) result2).getGridFsFileName()); + + assertThat(operations.findOne( + new Query().addCriteria(Criteria.where("filename").is(((LocalArtifact) result).getGridFsFileName())))) + .isNotNull(); + artifactManagement.deleteLocalArtifact(result.getId()); + assertThat(operations.findOne( + new Query().addCriteria(Criteria.where("filename").is(((LocalArtifact) result).getGridFsFileName())))) + .isNotNull(); + + artifactManagement.deleteLocalArtifact(result2.getId()); + assertThat(operations.findOne( + new Query().addCriteria(Criteria.where("filename").is(((LocalArtifact) result).getGridFsFileName())))) + .isNull(); + } + + /** + * Test method for + * {@link org.eclipse.hawkbit.repository.ArtifactManagement#findArtifact(java.lang.Long)} + * . + * + * @throws IOException + * @throws NoSuchAlgorithmException + */ + @Test + @Description("Loads an artifact based on given ID.") + public void findArtifact() throws NoSuchAlgorithmException, IOException { + SoftwareModule sm = new SoftwareModule(softwareManagement.findSoftwareModuleTypeByKey("os"), "name 1", + "version 1", null, null); + sm = softwareManagement.createSoftwareModule(sm); + + final Artifact result = artifactManagement.createLocalArtifact(new RandomGeneratedInputStream(5 * 1024), + sm.getId(), "file1", false); + + assertThat(artifactManagement.findArtifact(result.getId())).isEqualTo(result); + } + + /** + * Test method for + * {@link org.eclipse.hawkbit.repository.ArtifactManagement#loadLocalArtifactBinary(java.lang.Long)} + * . + * + * @throws IOException + * @throws NoSuchAlgorithmException + */ + @Test + @Description("Loads an artifact binary based on given ID.") + public void loadStreamOfLocalArtifact() throws NoSuchAlgorithmException, IOException { + SoftwareModule sm = new SoftwareModule(softwareManagement.findSoftwareModuleTypeByKey("os"), "name 1", + "version 1", null, null); + sm = softwareManagement.createSoftwareModule(sm); + + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + + final LocalArtifact result = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + sm.getId(), "file1", false); + + assertTrue(IOUtils.contentEquals(new ByteArrayInputStream(random), + artifactManagement.loadLocalArtifactBinary(result).getFileInputStream())); + } + + @Test(expected = InsufficientPermissionException.class) + @WithUser(allSpPermissions = true, removeFromAllPermission = { SpPermission.DOWNLOAD_REPOSITORY_ARTIFACT }) + @Description("Trys and fails to load an artifact without required permission. Checks if expected InsufficientPermissionException is thrown.") + public void loadLocalArtifactBinaryWithoutDownloadArtifactThrowsPermissionDenied() { + artifactManagement.loadLocalArtifactBinary(new LocalArtifact()); + } + + @Test + @Description("Searches an artifact through the relations of a software module.") + public void findLocalArtifactBySoftwareModule() { + SoftwareModule sm = new SoftwareModule(osType, "name 1", "version 1", null, null); + sm = softwareManagement.createSoftwareModule(sm); + + SoftwareModule sm2 = new SoftwareModule(osType, "name 2", "version 2", null, null); + sm2 = softwareManagement.createSoftwareModule(sm2); + + assertThat(artifactManagement.findLocalArtifactBySoftwareModule(pageReq, sm.getId())).isEmpty(); + + final Artifact result = artifactManagement.createLocalArtifact(new RandomGeneratedInputStream(5 * 1024), + sm.getId(), "file1", false); + + assertThat(artifactManagement.findLocalArtifactBySoftwareModule(pageReq, sm.getId())).hasSize(1); + } + + @Test + @Description("Searches an artifact through the relations of a software module and the filename.") + public void findByFilenameAndSoftwareModule() { + SoftwareModule sm = new SoftwareModule(osType, "name 1", "version 1", null, null); + sm = softwareManagement.createSoftwareModule(sm); + + assertThat(artifactManagement.findByFilenameAndSoftwareModule("file1", sm.getId())).isEmpty(); + + artifactManagement.createLocalArtifact(new RandomGeneratedInputStream(5 * 1024), sm.getId(), "file1", false); + artifactManagement.createLocalArtifact(new RandomGeneratedInputStream(5 * 1024), sm.getId(), "file2", false); + + assertThat(artifactManagement.findByFilenameAndSoftwareModule("file1", sm.getId())).hasSize(1); + + } +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/ControllerManagementTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/ControllerManagementTest.java new file mode 100755 index 000000000..8289b8173 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/ControllerManagementTest.java @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import static org.fest.assertions.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.RandomStringUtils; +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.junit.Test; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +@Features("Component Tests - Repository") +@Stories("Controller Management") +public class ControllerManagementTest extends AbstractIntegrationTest { + + @Test + @Description("Controller adds a new action status.") + public void controllerAddsActionStatus() { + final Target target = new Target("4712"); + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + Target savedTarget = targetManagement.createTarget(target); + + final List toAssign = new ArrayList(); + toAssign.add(savedTarget); + + assertThat(savedTarget.getTargetInfo().getUpdateStatus()).isEqualTo(TargetUpdateStatus.UNKNOWN); + + savedTarget = deploymentManagement.assignDistributionSet(ds, toAssign).getAssignedTargets().iterator().next(); + final Action savedAction = deploymentManagement.findActiveActionsByTarget(savedTarget).get(0); + + assertThat(targetManagement.findTargetByControllerID(savedTarget.getControllerId()).getTargetInfo() + .getUpdateStatus()).isEqualTo(TargetUpdateStatus.PENDING); + + ActionStatus actionStatusMessage = new ActionStatus(savedAction, Action.Status.RUNNING, + System.currentTimeMillis()); + actionStatusMessage.addMessage("foobar"); + savedAction.setStatus(Status.RUNNING); + controllerManagament.addUpdateActionStatus(actionStatusMessage, savedAction); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.PENDING); + + actionStatusMessage = new ActionStatus(savedAction, Action.Status.FINISHED, System.currentTimeMillis()); + actionStatusMessage.addMessage(RandomStringUtils.randomAscii(512)); + savedAction.setStatus(Status.FINISHED); + controllerManagament.addUpdateActionStatus(actionStatusMessage, savedAction); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.IN_SYNC); + + assertThat(actionStatusRepository.findAll(pageReq).getNumberOfElements()).isEqualTo(3); + assertThat(deploymentManagement.findActionStatusMessagesByActionInDescOrder(pageReq, savedAction, false) + .getNumberOfElements()).isEqualTo(3); + } + + @Test + @Description("Controller trys to finish an update process after it has been finished by an error action status.") + public void tryToFinishUpdateProcessMoreThenOnce() { + + // mock + final Target target = new Target("Rabbit"); + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + Target savedTarget = targetManagement.createTarget(target); + final List toAssign = new ArrayList(); + toAssign.add(savedTarget); + savedTarget = deploymentManagement.assignDistributionSet(ds, toAssign).getAssignedTargets().iterator().next(); + Action savedAction = deploymentManagement.findActiveActionsByTarget(savedTarget).get(0); + + // test and verify + final ActionStatus actionStatusMessage = new ActionStatus(savedAction, Action.Status.RUNNING, + System.currentTimeMillis()); + actionStatusMessage.addMessage("running"); + savedAction = controllerManagament.addUpdateActionStatus(actionStatusMessage, savedAction); + assertThat(targetManagement.findTargetByControllerID("Rabbit").getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.PENDING); + + final ActionStatus actionStatusMessage2 = new ActionStatus(savedAction, Action.Status.ERROR, + System.currentTimeMillis()); + actionStatusMessage2.addMessage("error"); + savedAction = controllerManagament.addUpdateActionStatus(actionStatusMessage2, savedAction); + assertThat(targetManagement.findTargetByControllerID("Rabbit").getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.ERROR); + + final ActionStatus actionStatusMessage3 = new ActionStatus(savedAction, Action.Status.FINISHED, + System.currentTimeMillis()); + actionStatusMessage3.addMessage("finish"); + savedAction = controllerManagament.addUpdateActionStatus(actionStatusMessage3, savedAction); + + targetManagement.findTargetByControllerID("Rabbit").getTargetInfo().getUpdateStatus(); + + // test + assertThat(targetManagement.findTargetByControllerID("Rabbit").getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.ERROR); + + } +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/DeploymentManagementTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/DeploymentManagementTest.java new file mode 100755 index 000000000..ba7506ac6 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/DeploymentManagementTest.java @@ -0,0 +1,1007 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.Constants; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.eventbus.event.CancelTargetAssignmentEvent; +import org.eclipse.hawkbit.eventbus.event.TargetAssignDistributionSetEvent; +import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.ActionWithStatusCount; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetTag; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort.Direction; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Issue; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * Test class testing the functionality of triggering a deployment of + * {@link DistributionSet}s to {@link Target}s. + * + */ +@Features("Component Tests - Repository") +@Stories("Deployment Management") +public class DeploymentManagementTest extends AbstractIntegrationTest { + + @Autowired + private EventBus eventBus; + + @Test + @Description("Test verifies that the custom query to find all actions include the count of action status is working correctly") + public void findActionsWithStatusCountByTarget() { + final DistributionSet testDs = TestDataUtil.generateDistributionSet("TestDs", "1.0", softwareManagement, + distributionSetManagement, new ArrayList()); + final List testTarget = targetManagement.createTargets(TestDataUtil.generateTargets(1)); + // one action with one action status is generated + final Action action = deploymentManagement.assignDistributionSet(testDs, testTarget).getActions().get(0); + // save 2 action status + actionStatusRepository.save(new ActionStatus(action, Status.RETRIEVED, System.currentTimeMillis())); + actionStatusRepository.save(new ActionStatus(action, Status.RUNNING, System.currentTimeMillis())); + + final List findActionsWithStatusCountByTarget = deploymentManagement + .findActionsWithStatusCountByTargetOrderByIdDesc(testTarget.get(0)); + + assertThat(findActionsWithStatusCountByTarget).hasSize(1); + assertThat(findActionsWithStatusCountByTarget.get(0).getActionStatusCount()).isEqualTo(3L); + } + + @Test + @Description("Test verifies that an assignment with automatic cancelation works correctly even if the update is split into multiple partitions on the database.") + @Issue("MECS-674") + public void multiAssigmentHistoryOverMultiplePagesResultsInTwoActiveAction() { + + final DistributionSet cancelDs = TestDataUtil.generateDistributionSet("Canceled DS", "1.0", softwareManagement, + distributionSetManagement, new ArrayList()); + + final DistributionSet cancelDs2 = TestDataUtil.generateDistributionSet("Canceled DS", "1.2", softwareManagement, + distributionSetManagement, new ArrayList()); + + List targets = targetManagement + .createTargets(TestDataUtil.generateTargets(Constants.MAX_ENTRIES_IN_STATEMENT + 10)); + + targets = deploymentManagement.assignDistributionSet(cancelDs, targets).getAssignedTargets(); + targets = deploymentManagement.assignDistributionSet(cancelDs2, targets).getAssignedTargets(); + + targetManagement.findAllTargetIds().forEach(targetIdName -> { + assertThat(deploymentManagement.findActiveActionsByTarget( + targetManagement.findTargetByControllerID(targetIdName.getControllerId()))).hasSize(2); + }); + } + + @Test + @Description("Cancels multiple active actions on a target. Expected behaviour is that with two active " + + "actions after canceling the second active action the first one is still running as it is not touched by the cancelation. After canceling the first one " + + "also the target goes back to IN_SYNC as no open action is left.") + public void manualCancelWithMultipleAssignmentsCancelLastOneFirst() { + Target target = new Target("4712"); + final DistributionSet dsFirst = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement, true); + dsFirst.setRequiredMigrationStep(true); + final DistributionSet dsSecond = TestDataUtil.generateDistributionSet("2", softwareManagement, + distributionSetManagement, true); + final DistributionSet dsInstalled = TestDataUtil.generateDistributionSet("installed", softwareManagement, + distributionSetManagement, true); + + target.getTargetInfo().setInstalledDistributionSet(dsInstalled); + target = targetManagement.createTarget(target); + + // check initial status + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.UNKNOWN); + + // assign the two sets in a row + Action firstAction = assignSet(target, dsFirst); + Action secondAction = assignSet(target, dsSecond); + + assertThat(actionRepository.findAll()).hasSize(2); + assertThat(actionStatusRepository.findAll()).hasSize(2); + + // we cancel second -> back to first + deploymentManagement.cancelAction(secondAction, + targetManagement.findTargetByControllerID(target.getControllerId())); + secondAction = deploymentManagement.findActionWithDetails(secondAction.getId()); + // confirm cancellation + secondAction.setStatus(Status.CANCELED); + controllerManagement.addCancelActionStatus( + new ActionStatus(secondAction, Status.CANCELED, System.currentTimeMillis()), secondAction); + assertThat(actionStatusRepository.findAll()).hasSize(4); + assertThat(targetManagement.findTargetByControllerID("4712").getAssignedDistributionSet()).isEqualTo(dsFirst); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.PENDING); + + // we cancel first -> back to installed + deploymentManagement.cancelAction(firstAction, + targetManagement.findTargetByControllerID(target.getControllerId())); + firstAction = deploymentManagement.findActionWithDetails(firstAction.getId()); + // confirm cancellation + firstAction.setStatus(Status.CANCELED); + controllerManagement.addCancelActionStatus( + new ActionStatus(firstAction, Status.CANCELED, System.currentTimeMillis()), firstAction); + assertThat(actionStatusRepository.findAll()).hasSize(6); + assertThat(targetManagement.findTargetByControllerID("4712").getAssignedDistributionSet()) + .isEqualTo(dsInstalled); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.IN_SYNC); + } + + @Test + @Description("Cancels multiple active actions on a target. Expected behaviour is that with two active " + + "actions after canceling the first active action the system switched to second one. After canceling this one " + + "also the target goes back to IN_SYNC as no open action is left.") + public void manualCancelWithMultipleAssignmentsCancelMiddleOneFirst() { + Target target = new Target("4712"); + final DistributionSet dsFirst = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement, true); + dsFirst.setRequiredMigrationStep(true); + final DistributionSet dsSecond = TestDataUtil.generateDistributionSet("2", softwareManagement, + distributionSetManagement, true); + final DistributionSet dsInstalled = TestDataUtil.generateDistributionSet("installed", softwareManagement, + distributionSetManagement, true); + + target.getTargetInfo().setInstalledDistributionSet(dsInstalled); + target = targetManagement.createTarget(target); + + // check initial status + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.UNKNOWN); + + // assign the two sets in a row + Action firstAction = assignSet(target, dsFirst); + Action secondAction = assignSet(target, dsSecond); + + assertThat(actionRepository.findAll()).hasSize(2); + assertThat(actionStatusRepository.findAll()).hasSize(2); + + // we cancel first -> second is left + deploymentManagement.cancelAction(firstAction, + targetManagement.findTargetByControllerID(target.getControllerId())); + // confirm cancellation + firstAction = deploymentManagement.findActionWithDetails(firstAction.getId()); + firstAction.setStatus(Status.CANCELED); + controllerManagement.addCancelActionStatus( + new ActionStatus(firstAction, Status.CANCELED, System.currentTimeMillis()), firstAction); + assertThat(actionStatusRepository.findAll()).hasSize(4); + assertThat(targetManagement.findTargetByControllerID("4712").getAssignedDistributionSet()).isEqualTo(dsSecond); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.PENDING); + + // we cancel second -> remain assigned until finished cancellation + deploymentManagement.cancelAction(secondAction, + targetManagement.findTargetByControllerID(target.getControllerId())); + secondAction = deploymentManagement.findActionWithDetails(secondAction.getId()); + assertThat(actionStatusRepository.findAll()).hasSize(5); + assertThat(targetManagement.findTargetByControllerID("4712").getAssignedDistributionSet()).isEqualTo(dsSecond); + // confirm cancellation + secondAction.setStatus(Status.CANCELED); + controllerManagement.addCancelActionStatus( + new ActionStatus(secondAction, Status.CANCELED, System.currentTimeMillis()), secondAction); + // cancelled success -> back to dsInstalled + assertThat(targetManagement.findTargetByControllerID("4712").getAssignedDistributionSet()) + .isEqualTo(dsInstalled); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.IN_SYNC); + } + + private Action assignSet(final Target target, final DistributionSet ds) { + deploymentManagement.assignDistributionSet(ds.getId(), new String[] { target.getControllerId() }); + assertThat( + targetManagement.findTargetByControllerID(target.getControllerId()).getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.PENDING); + assertThat(targetManagement.findTargetByControllerID(target.getControllerId()).getAssignedDistributionSet()) + .isEqualTo(ds); + final Action action = actionRepository.findByTargetAndDistributionSet(pageReq, target, ds).getContent().get(0); + assertThat(action).isNotNull(); + return action; + } + + /** + * test a simple deployment by calling the + * {@link TargetRepository#assignDistributionSet(DistributionSet, Iterable)} + * and checking the active action and the action history of the targets. + * + * @throws InterruptedException + */ + @Test + @Description("Simple deployment or distribution set to target assignment test.") + public void assignDistributionSet2Targets() throws InterruptedException { + + final EventHandlerMock eventHandlerMock = new EventHandlerMock(20); + eventBus.register(eventHandlerMock); + + final String myCtrlIDPref = "myCtrlID"; + final Iterable savedNakedTargets = targetManagement + .createTargets(TestDataUtil.buildTargetFixtures(10, myCtrlIDPref, "first description")); + + final String myDeployedCtrlIDPref = "myDeployedCtrlID"; + final List savedDeployedTargets = targetManagement + .createTargets(TestDataUtil.buildTargetFixtures(20, myDeployedCtrlIDPref, "first description")); + + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + deploymentManagement.assignDistributionSet(ds, savedDeployedTargets); + + // verify that one Action for each assignDistributionSet + assertThat(actionRepository.findAll(pageReq).getNumberOfElements()).isEqualTo(20); + + final Iterable allFoundTargets = targetManagement.findTargetsAll(pageReq).getContent(); + + assertThat(allFoundTargets).containsAll(savedDeployedTargets).containsAll(savedNakedTargets); + assertThat(savedDeployedTargets).doesNotContain(Iterables.toArray(savedNakedTargets, Target.class)); + assertThat(savedNakedTargets).doesNotContain(Iterables.toArray(savedDeployedTargets, Target.class)); + + for (final Target myt : savedNakedTargets) { + final Target t = targetManagement.findTargetByControllerID(myt.getControllerId()); + assertThat(deploymentManagement.findActionsByTarget(t)).isEmpty(); + } + + for (final Target myt : savedDeployedTargets) { + final Target t = targetManagement.findTargetByControllerID(myt.getControllerId()); + final List activeActionsByTarget = deploymentManagement.findActiveActionsByTarget(t); + assertThat(activeActionsByTarget).isNotEmpty(); + assertThat(t.getTargetInfo().getUpdateStatus()).isEqualTo(TargetUpdateStatus.PENDING); + for (final Action ua : activeActionsByTarget) { + assertThat(ua.getDistributionSet()).isEqualTo(ds); + } + } + + final List events = eventHandlerMock.getEvents(10, TimeUnit.SECONDS); + + assertTargetAssignDistributionSetEvents(savedDeployedTargets, ds, events); + } + + @Test + @Description("Test that it is not possible to assign a distribution set that is not complete.") + public void failDistributionSetAssigmentThatIsNotComplete() throws InterruptedException { + final EventHandlerMock eventHandlerMock = new EventHandlerMock(0); + eventBus.register(eventHandlerMock); + + final List targets = targetManagement.createTargets(TestDataUtil.generateTargets(10)); + + final SoftwareModule ah = softwareManagement + .createSoftwareModule(new SoftwareModule(appType, "agent-hub", "1.0.1", null, "")); + final SoftwareModule jvm = softwareManagement + .createSoftwareModule(new SoftwareModule(runtimeType, "oracle-jre", "1.7.2", null, "")); + final SoftwareModule os = softwareManagement + .createSoftwareModule(new SoftwareModule(osType, "poky", "3.0.2", null, "")); + + final DistributionSet incomplete = distributionSetManagement.createDistributionSet( + new DistributionSet("incomplete", "v1", "", standardDsType, Lists.newArrayList(ah, jvm))); + + try { + deploymentManagement.assignDistributionSet(incomplete, targets); + fail("expected IncompleteDistributionSetException"); + } catch (final IncompleteDistributionSetException ex) { + } + + incomplete.addModule(os); + final DistributionSet nowComplete = distributionSetManagement.updateDistributionSet(incomplete); + + // give some chance to receive events asynchronously + Thread.sleep(300); + final List events = eventHandlerMock.getEvents(1, TimeUnit.MILLISECONDS); + assertThat(events).isEmpty(); + + final EventHandlerMock eventHandlerMockAfterCompletionOfDs = new EventHandlerMock(10); + eventBus.register(eventHandlerMockAfterCompletionOfDs); + + assertThat(deploymentManagement.assignDistributionSet(nowComplete, targets).getAssigned()).isEqualTo(10); + assertTargetAssignDistributionSetEvents(targets, nowComplete, + eventHandlerMockAfterCompletionOfDs.getEvents(10, TimeUnit.SECONDS)); + } + + @Test + @Description("Multiple deployments or distribution set to target assignment test. Expected behaviour is that a new deployment " + + "overides unfinished old one which are canceled as part of the operation.") + public void mutipleDeployments() throws InterruptedException { + final String undeployedTargetPrefix = "undep-T"; + final int noOfUndeployedTargets = 5; + + final String deployedTargetPrefix = "dep-T"; + final int noOfDeployedTargets = 4; + + final int noOfDistributionSets = 3; + + // Each of the four targets get one assignment (4 * 1 = 4) + final int expectedNumberOfEventsForAssignment = 4; + final EventHandlerMock eventHandlerMock = new EventHandlerMock(expectedNumberOfEventsForAssignment); + eventBus.register(eventHandlerMock); + + // Each of the four targets get two more assignment the which are + // cancelled (4 * 2 = 8) + final int expectedNumberOfEventsForCancel = 8; + final CancelEventHandlerMock cancelEventHandlerMock = new CancelEventHandlerMock( + expectedNumberOfEventsForCancel); + eventBus.register(cancelEventHandlerMock); + + final DeploymentResult deploymentResult = prepareComplexRepo(undeployedTargetPrefix, noOfUndeployedTargets, + deployedTargetPrefix, noOfDeployedTargets, noOfDistributionSets, "myTestDS"); + + final List deployedTargetIDs = deploymentResult.getDeployedTargetIDs(); + final List undeployedTargetIDs = deploymentResult.getUndeployedTargetIDs(); + final List savedNakedTargets = deploymentResult.getUndeployedTargets(); + final List savedDeployedTargets = deploymentResult.getDeployedTargets(); + + // retrieving all Actions created by the assignDistributionSet call + final Page page = actionRepository.findAll(pageReq); + // and verify the number + assertThat(page.getTotalElements()).isEqualTo(noOfDeployedTargets * noOfDistributionSets); + + // only records retrieved from the DB can be evaluated to be sure that + // all fields are + // populated; + final Iterable allFoundTargets = targetRepository.findAll(); + + final Iterable deployedTargetsFromDB = targetRepository.findAll(deployedTargetIDs); + final Iterable undeployedTargetsFromDB = targetRepository.findAll(undeployedTargetIDs); + + // test that number of Targets + assertThat(allFoundTargets.spliterator().getExactSizeIfKnown()) + .isEqualTo(deployedTargetsFromDB.spliterator().getExactSizeIfKnown() + + undeployedTargetsFromDB.spliterator().getExactSizeIfKnown()); + assertThat(deployedTargetsFromDB.spliterator().getExactSizeIfKnown()).isEqualTo(noOfDeployedTargets); + assertThat(undeployedTargetsFromDB.spliterator().getExactSizeIfKnown()).isEqualTo(noOfUndeployedTargets); + + // test the content of different lists + assertThat(allFoundTargets).containsAll(deployedTargetsFromDB).containsAll(undeployedTargetsFromDB); + assertThat(deployedTargetsFromDB).containsAll(savedDeployedTargets) + .doesNotContain(Iterables.toArray(undeployedTargetsFromDB, Target.class)); + assertThat(undeployedTargetsFromDB).containsAll(savedNakedTargets) + .doesNotContain(Iterables.toArray(deployedTargetsFromDB, Target.class)); + + // For each of the 4 targets 1 distribution sets gets assigned + eventHandlerMock.getEvents(10, TimeUnit.SECONDS); + + // For each of the 4 targets 2 distribution sets gets cancelled + cancelEventHandlerMock.getEvents(10, TimeUnit.SECONDS); + + } + + @Test + @Description("Multiple deployments or distribution set to target assignment test including finished response " + + "from target/controller. Expected behaviour is that in case of OK finished update the target will go to " + + "IN_SYNC status and installed DS is set to the assigned DS entry.") + public void assignDistributionSetAndAddFinishedActionStatus() { + final PageRequest pageRequest = new PageRequest(0, 100, Direction.ASC, ActionStatusFields.ID.getFieldName()); + + final DeploymentResult deployResWithDsA = prepareComplexRepo("undep-A-T", 2, "dep-A-T", 4, 1, "dsA"); + final DeploymentResult deployResWithDsB = prepareComplexRepo("undep-B-T", 3, "dep-B-T", 5, 1, "dsB"); + final DeploymentResult deployResWithDsC = prepareComplexRepo("undep-C-T", 4, "dep-C-T", 6, 1, "dsC"); + + // keep a reference to the created DistributionSets + final DistributionSet dsA = deployResWithDsA.getDistributionSets().get(0); + final DistributionSet dsB = deployResWithDsB.getDistributionSets().get(0); + final DistributionSet dsC = deployResWithDsC.getDistributionSets().get(0); + + // retrieving the UpdateActions created by the assignments + final Action updActA = actionRepository.findByDistributionSet(pageRequest, dsA).getContent().get(0); + final Action updActB = actionRepository.findByDistributionSet(pageRequest, dsB).getContent().get(0); + final Action updActC = actionRepository.findByDistributionSet(pageRequest, dsC).getContent().get(0); + + // verifying the correctness of the assignments + for (final Target t : deployResWithDsA.getDeployedTargets()) { + assertThat(t.getAssignedDistributionSet().getId()).isEqualTo(dsA.getId()); + assertThat(targetManagement.findTargetByControllerID(t.getControllerId()).getTargetInfo() + .getInstalledDistributionSet()).isNull(); + } + for (final Target t : deployResWithDsB.getDeployedTargets()) { + assertThat(t.getAssignedDistributionSet().getId()).isEqualTo(dsB.getId()); + assertThat(targetManagement.findTargetByControllerID(t.getControllerId()).getTargetInfo() + .getInstalledDistributionSet()).isNull(); + } + for (final Target t : deployResWithDsC.getDeployedTargets()) { + assertThat(t.getAssignedDistributionSet().getId()).isEqualTo(dsC.getId()); + assertThat(targetManagement.findTargetByControllerID(t.getControllerId()).getTargetInfo() + .getInstalledDistributionSet()).isNull(); + assertThat(targetManagement.findTargetByControllerID(t.getControllerId()).getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.PENDING); + } + + final List updatedTsDsA = sendUpdateActionStatusToTargets(dsA, deployResWithDsA.getDeployedTargets(), + Status.FINISHED, new String[] { "alles gut" }); + + // verify, that dsA is deployed correctly + assertThat(updatedTsDsA).isEqualTo(deployResWithDsA.getDeployedTargets()); + for (final Target t_ : updatedTsDsA) { + final Target t = targetManagement.findTargetByControllerID(t_.getControllerId()); + assertThat(t.getAssignedDistributionSet()).isEqualTo(dsA); + assertThat(targetManagement.findTargetByControllerID(t.getControllerId()).getTargetInfo() + .getInstalledDistributionSet()).isEqualTo(dsA); + assertThat(targetManagement.findTargetByControllerID(t.getControllerId()).getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.IN_SYNC); + assertThat(deploymentManagement.findActiveActionsByTarget(t)).hasSize(0); + } + + // deploy dsA to the target which already have dsB deployed -> must + // remove updActB from + // activeActions, add a corresponding cancelAction and another + // UpdateAction for dsA + final Iterable deployed2DS = deploymentManagement + .assignDistributionSet(dsA, deployResWithDsB.getDeployedTargets()).getAssignedTargets(); + final Action updActA2 = actionRepository.findByDistributionSet(pageRequest, dsA).getContent().get(1); + + assertThat(deployed2DS).containsAll(deployResWithDsB.getDeployedTargets()); + assertThat(deployed2DS).hasSameSizeAs(deployResWithDsB.getDeployedTargets()); + + for (final Target t_ : deployed2DS) { + final Target t = targetManagement.findTargetByControllerID(t_.getControllerId()); + assertThat(t.getAssignedDistributionSet()).isEqualTo(dsA); + assertThat(targetManagement.findTargetByControllerID(t.getControllerId()).getTargetInfo() + .getInstalledDistributionSet()).isNull(); + assertThat(targetManagement.findTargetByControllerID(t.getControllerId()).getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.PENDING); + + } + } + + /** + * test the deletion of {@link DistributionSet}s including exception in case + * of {@link Target}s are assigned by + * {@link Target#getAssignedDistributionSet()} or + * {@link Target#getInstalledDistributionSet()} + */ + @Test + @Description("Deletes distribution set. Expected behaviour is that a soft delete is performed " + + "if the DS is assigned to a target and a hard delete if the DS is not in use at all.") + public void deleteDistributionSet() { + + final PageRequest pageRequest = new PageRequest(0, 100, Direction.ASC, "id"); + + final String undeployedTargetPrefix = "undep-T"; + final int noOfUndeployedTargets = 2; + + final String deployedTargetPrefix = "dep-T"; + final int noOfDeployedTargets = 4; + + final int noOfDistributionSets = 3; + + final DeploymentResult deploymentResult = prepareComplexRepo(undeployedTargetPrefix, noOfUndeployedTargets, + deployedTargetPrefix, noOfDeployedTargets, noOfDistributionSets, "myTestDS"); + + DistributionSet dsA = TestDataUtil.generateDistributionSet("", softwareManagement, distributionSetManagement); + + distributionSetManagement.deleteDistributionSet(dsA.getId()); + dsA = distributionSetManagement.findDistributionSetById(dsA.getId()); + assertThat(dsA).isNull(); + + // // verify that the ds is not physically deleted + for (final DistributionSet ds : deploymentResult.getDistributionSets()) { + distributionSetManagement.deleteDistributionSet(ds.getId()); + final DistributionSet foundDS = distributionSetManagement.findDistributionSetById(ds.getId()); + assertThat(foundDS).isNotNull(); + assertThat(foundDS.isDeleted()).isTrue(); + } + + // verify that deleted attribute is used correctly + List allFoundDS = distributionSetManagement.findDistributionSetsAll(pageReq, false, true) + .getContent(); + assertThat(allFoundDS.size()).isEqualTo(0); + allFoundDS = distributionSetManagement.findDistributionSetsAll(pageRequest, true, true).getContent(); + assertThat(allFoundDS).hasSize(noOfDistributionSets); + + for (final DistributionSet ds : deploymentResult.getDistributionSets()) { + sendUpdateActionStatusToTargets(ds, deploymentResult.getDeployedTargets(), Status.FINISHED, + "blabla alles gut"); + } + // try to delete again + distributionSetManagement.deleteDistributionSet(deploymentResult.getDistributionSetIDs() + .toArray(new Long[deploymentResult.getDistributionSetIDs().size()])); + // verify that the result is the same, even though distributionSet dsA + // has been installed + // successfully and no activeAction is referring to created distribution + // sets + allFoundDS = distributionSetManagement.findDistributionSetsAll(pageRequest, false, true).getContent(); + assertThat(allFoundDS.size()).isEqualTo(0); + allFoundDS = distributionSetManagement.findDistributionSetsAll(pageRequest, true, true).getContent(); + assertThat(allFoundDS).hasSize(noOfDistributionSets); + + } + + @Test + @Description("Deletes multiple targets and verfies that all related metadat is also deleted.") + public void deletesTargetsAndVerifyCascadeDeletes() { + + final String undeployedTargetPrefix = "undep-T"; + final int noOfUndeployedTargets = 2; + + final String deployedTargetPrefix = "dep-T"; + final int noOfDeployedTargets = 4; + + final int noOfDistributionSets = 3; + + final DeploymentResult deploymentResult = prepareComplexRepo(undeployedTargetPrefix, noOfUndeployedTargets, + deployedTargetPrefix, noOfDeployedTargets, noOfDistributionSets, "myTestDS"); + + for (final DistributionSet ds : deploymentResult.getDistributionSets()) { + sendUpdateActionStatusToTargets(ds, deploymentResult.getDeployedTargets(), Status.FINISHED, + "blabla alles gut"); + } + + assertThat(targetManagement.countTargetsAll()).isNotZero(); + assertThat(actionStatusRepository.count()).isNotZero(); + + targetManagement + .deleteTargets(deploymentResult.getUndeployedTargetIDs().toArray(new Long[noOfUndeployedTargets])); + targetManagement.deleteTargets(deploymentResult.getDeployedTargetIDs().toArray(new Long[noOfDeployedTargets])); + + assertThat(targetManagement.countTargetsAll()).isZero(); + assertThat(actionStatusRepository.count()).isZero(); + } + + private List sendUpdateActionStatusToTargets(final DistributionSet dsA, final Iterable targs, + final Status status, final String... msgs) { + final List result = new ArrayList(); + for (final Target t : targs) { + final List findByTarget = actionRepository.findByTarget(t); + for (final Action action : findByTarget) { + result.add(sendUpdateActionStatusToTarget(status, action, t, msgs)); + } + } + return result; + } + + private Target sendUpdateActionStatusToTarget(final Status status, final Action updActA, final Target t, + final String... msgs) { + updActA.setStatus(status); + + final ActionStatus statusMessages = new ActionStatus(); + statusMessages.setAction(updActA); + statusMessages.setOccurredAt(System.currentTimeMillis()); + statusMessages.setStatus(status); + for (final String msg : msgs) { + statusMessages.addMessage(msg); + } + controllerManagament.addUpdateActionStatus(statusMessages, updActA); + return targetManagement.findTargetByControllerID(t.getControllerId()); + } + + @Test + @Description("Testing if changing target and the status without refreshing the entities from the DB (e.g. concurrent changes from UI and from controller) works") + public void alternatingAssignmentAndAddUpdateActionStatus() { + + final DistributionSet dsA = TestDataUtil.generateDistributionSet("a", softwareManagement, + distributionSetManagement); + final DistributionSet dsB = TestDataUtil.generateDistributionSet("b", softwareManagement, + distributionSetManagement); + Target targ = targetManagement + .createTarget(TestDataUtil.buildTargetFixture("target-id-A", "first description")); + + List targs = new ArrayList(); + targs.add(targ); + + // doing the assignment + targs = deploymentManagement.assignDistributionSet(dsA, targs).getAssignedTargets(); + targ = targetManagement.findTargetByControllerID(targs.iterator().next().getControllerId()); + + // checking the revisions of the created entities + // verifying that the revision of the object and the revision within the + // DB has not changed + assertThat(dsA.getOptLockRevision()).isEqualTo( + distributionSetManagement.findDistributionSetByIdWithDetails(dsA.getId()).getOptLockRevision()); + + // verifying that the assignment is correct + assertEquals(1, deploymentManagement.findActiveActionsByTarget(targ).size()); + assertEquals(1, deploymentManagement.findActionsByTarget(targ).size()); + assertEquals(TargetUpdateStatus.PENDING, targ.getTargetInfo().getUpdateStatus()); + assertEquals(dsA, targ.getAssignedDistributionSet()); + assertEquals(dsA, deploymentManagement.findActiveActionsByTarget(targ).get(0).getDistributionSet()); + assertNull(targ.getTargetInfo().getInstalledDistributionSet()); + + final Page updAct = actionRepository.findByDistributionSet(pageReq, dsA); + final Action action = updAct.getContent().get(0); + action.setStatus(Status.FINISHED); + final ActionStatus statusMessage = new ActionStatus(action, Status.FINISHED, System.currentTimeMillis(), ""); + controllerManagament.addUpdateActionStatus(statusMessage, action); + + targ = targetManagement.findTargetByControllerID(targ.getControllerId()); + + assertEquals(0, deploymentManagement.findActiveActionsByTarget(targ).size()); + // try { + assertEquals(1, deploymentManagement.findInActiveActionsByTarget(targ).size()); + // } + // catch( final LazyInitializationException ex ) { + // + // } + assertEquals(TargetUpdateStatus.IN_SYNC, targ.getTargetInfo().getUpdateStatus()); + assertEquals(dsA, targ.getAssignedDistributionSet()); + assertEquals(dsA, targ.getTargetInfo().getInstalledDistributionSet()); + + targs = deploymentManagement.assignDistributionSet(dsB.getId(), new String[] { "target-id-A" }) + .getAssignedTargets(); + + targ = targs.iterator().next(); + + assertEquals(1, deploymentManagement.findActiveActionsByTarget(targ).size()); + assertEquals(TargetUpdateStatus.PENDING, + targetManagement.findTargetByControllerID(targ.getControllerId()).getTargetInfo().getUpdateStatus()); + assertEquals(dsB, targ.getAssignedDistributionSet()); + assertEquals(dsA.getId(), targetManagement.findTargetByControllerIDWithDetails(targ.getControllerId()) + .getTargetInfo().getInstalledDistributionSet().getId()); + assertEquals(dsB, deploymentManagement.findActiveActionsByTarget(targ).get(0).getDistributionSet()); + + } + + @Test + @Description("The test verfies that the DS itself is not changed because of an target assignment" + + " which is a relationship but not a changed on the entity itself..") + public void checkThatDsRevisionsIsNotChangedWithTargetAssignment() { + final DistributionSet dsA = TestDataUtil.generateDistributionSet("a", softwareManagement, + distributionSetManagement); + final DistributionSet dsB = TestDataUtil.generateDistributionSet("b", softwareManagement, + distributionSetManagement); + Target targ = targetManagement + .createTarget(TestDataUtil.buildTargetFixture("target-id-A", "first description")); + + assertThat(dsA.getOptLockRevision()).isEqualTo( + distributionSetManagement.findDistributionSetByIdWithDetails(dsA.getId()).getOptLockRevision()); + + final List targs = new ArrayList(); + targs.add(targ); + final Iterable savedTargs = deploymentManagement.assignDistributionSet(dsA, targs).getAssignedTargets(); + targ = savedTargs.iterator().next(); + + assertThat(dsA.getOptLockRevision()).isEqualTo( + distributionSetManagement.findDistributionSetByIdWithDetails(dsA.getId()).getOptLockRevision()); + } + + @Test + @Description("Tests the switch from a soft to hard update by API") + public void forceSoftAction() { + // prepare + final Target target = targetManagement.createTarget(new Target("knownControllerId")); + final DistributionSet ds = TestDataUtil.generateDistributionSet("a", softwareManagement, + distributionSetManagement); + // assign ds to create an action + final DistributionSetAssignmentResult assignDistributionSet = deploymentManagement + .assignDistributionSet(ds.getId(), ActionType.SOFT, Action.NO_FORCE_TIME, target.getControllerId()); + final Action action = assignDistributionSet.getActions().get(0); + // verify preparation + Action findAction = deploymentManagement.findAction(action.getId()); + assertThat(findAction.getActionType()).isEqualTo(ActionType.SOFT); + + // test + deploymentManagement.forceTargetAction(action.getId()); + + // verify test + findAction = deploymentManagement.findAction(action.getId()); + assertThat(findAction.getActionType()).isEqualTo(ActionType.FORCED); + } + + @Test + @Description("Tests the switch from a hard to hard update by API, e.g. which in fact should not change anything.") + public void forceAlreadyForcedActionNothingChanges() { + // prepare + final Target target = targetManagement.createTarget(new Target("knownControllerId")); + final DistributionSet ds = TestDataUtil.generateDistributionSet("a", softwareManagement, + distributionSetManagement); + // assign ds to create an action + final DistributionSetAssignmentResult assignDistributionSet = deploymentManagement + .assignDistributionSet(ds.getId(), ActionType.FORCED, Action.NO_FORCE_TIME, target.getControllerId()); + final Action action = assignDistributionSet.getActions().get(0); + // verify perparation + Action findAction = deploymentManagement.findAction(action.getId()); + assertThat(findAction.getActionType()).isEqualTo(ActionType.FORCED); + + // test + final Action forceTargetAction = deploymentManagement.forceTargetAction(action.getId()); + + // verify test + assertThat(forceTargetAction.getActionType()).isEqualTo(ActionType.FORCED); + findAction = deploymentManagement.findAction(action.getId()); + assertThat(findAction.getActionType()).isEqualTo(ActionType.FORCED); + } + + /** + * Helper methods that creates 2 lists of targets and a list of distribution + * sets. + *

+ * All created distribution sets are assigned to all targets of the + * target list deployedTargets. + * + * @param undeployedTargetPrefix + * prefix to be used as target controller prefix + * @param noOfUndeployedTargets + * number of targets which remain undeployed + * @param deployedTargetPrefix + * prefix to be used as target controller prefix + * @param noOfDeployedTargets + * number of targets to which the created distribution sets + * assigned + * @param noOfDistributionSets + * number of distribution sets + * @param distributionSetPrefix + * prefix for the created distribution sets + * @return the {@link DeploymentResult} containing all created targets, the + * distribution sets, the corresponding IDs for later evaluation in + * tests + */ + private DeploymentResult prepareComplexRepo(final String undeployedTargetPrefix, final int noOfUndeployedTargets, + final String deployedTargetPrefix, final int noOfDeployedTargets, final int noOfDistributionSets, + final String distributionSetPrefix) { + final Iterable nakedTargets = targetManagement.createTargets( + TestDataUtil.buildTargetFixtures(noOfUndeployedTargets, undeployedTargetPrefix, "first description")); + + List deployedTargets = targetManagement.createTargets( + TestDataUtil.buildTargetFixtures(noOfDeployedTargets, deployedTargetPrefix, "first description")); + + // creating 10 DistributionSets + final List dsList = TestDataUtil.generateDistributionSets(distributionSetPrefix, + noOfDistributionSets, softwareManagement, distributionSetManagement); + String time = String.valueOf(System.currentTimeMillis()); + time = time.substring(time.length() - 5); + + // assigning all DistributionSet to the Target in the list + // deployedTargets + for (final DistributionSet ds : dsList) { + deployedTargets = deploymentManagement.assignDistributionSet(ds, deployedTargets).getAssignedTargets(); + } + + final DeploymentResult deploymentResult = new DeploymentResult(deployedTargets, nakedTargets, dsList, + deployedTargetPrefix, undeployedTargetPrefix, distributionSetPrefix); + return deploymentResult; + + } + + private void assertTargetAssignDistributionSetEvents(final List targets, final DistributionSet ds, + final List events) { + for (final Target myt : targets) { + boolean found = false; + for (final TargetAssignDistributionSetEvent event : events) { + if (event.getControllerId().equals(myt.getControllerId())) { + found = true; + final List activeActionsByTarget = deploymentManagement.findActiveActionsByTarget(myt); + assertThat(activeActionsByTarget).isNotEmpty(); + assertThat(event.getActionId()).isEqualTo(activeActionsByTarget.get(0).getId()) + .as("Action id in database and event do not match"); + assertThat(event.getSoftwareModules()) + .containsOnly(ds.getModules().toArray(new SoftwareModule[ds.getModules().size()])); + } + } + assertThat(found).isTrue().as("No event found for controller " + myt.getControllerId()); + } + } + + /** + * + * + */ + private class DeploymentResult + + { + final List deployedTargetIDs = new ArrayList(); + final List undeployedTargetIDs = new ArrayList(); + final List distributionSetIDs = new ArrayList(); + + private final List undeployedTargets = new ArrayList(); + private final List deployedTargets = new ArrayList(); + private final List distributionSets = new ArrayList(); + + private final String undeployedTargetPrefix; + private final String deployedTargetPrefix; + private final String distributionSetPrefix; + + /** + * @param deployedTargetPrefix + * @param undeployedTargetPrefix + * @param distributionSetPrefix + * @param savedDeployedTargets + * @param savedNakedTargets + * @param distributionSets + */ + public DeploymentResult(final Iterable deployedTs, final Iterable undeployedTs, + final Iterable dss, final String deployedTargetPrefix, + final String undeployedTargetPrefix, final String distributionSetPrefix) { + + this.undeployedTargetPrefix = undeployedTargetPrefix; + this.deployedTargetPrefix = deployedTargetPrefix; + this.distributionSetPrefix = distributionSetPrefix; + + Iterables.addAll(deployedTargets, deployedTs); + Iterables.addAll(undeployedTargets, undeployedTs); + Iterables.addAll(distributionSets, dss); + + deployedTargets.forEach(new Consumer() { + @Override + public void accept(final Target t) { + deployedTargetIDs.add(t.getId()); + } + }); + + undeployedTargets.forEach(new Consumer() { + @Override + public void accept(final Target t) { + undeployedTargetIDs.add(t.getId()); + } + }); + + distributionSets.forEach(new Consumer() { + @Override + public void accept(final DistributionSet ds) { + distributionSetIDs.add(ds.getId()); + } + }); + + } + + /** + * @return the distributionSetIDs + */ + public List getDistributionSetIDs() { + return distributionSetIDs; + } + + /** + * @return + */ + public List getDeployedTargetIDs() { + return deployedTargetIDs; + } + + /** + * @return + */ + public List getUndeployedTargets() { + return undeployedTargets; + } + + /** + * @return + */ + public List getDistributionSets() { + return distributionSets; + } + + /** + * @return + */ + public List getDeployedTargets() { + return deployedTargets; + } + + /** + * @return the noOfUndeployedTargets + */ + public int getNoOfUndeployedTargets() { + return undeployedTargetIDs.size(); + } + + /** + * @return the noOfDeployedTargets + */ + public int getNoOfDeployedTargets() { + return deployedTargetIDs.size(); + } + + /** + * @return the noOfDistributionSets + */ + public int getNoOfDistributionSets() { + return distributionSets.size(); + } + + /** + * @return the undeployedTargetIDs + */ + public List getUndeployedTargetIDs() { + return undeployedTargetIDs; + } + + /** + * @return the undeployedTargetPrefix + */ + public String getUndeployedTargetPrefix() { + return undeployedTargetPrefix; + } + + /** + * @return the deployedTargetPrefix + */ + public String getDeployedTargetPrefix() { + return deployedTargetPrefix; + } + + /** + * @return the distributionSetPrefix + */ + public String getDistributionSetPrefix() { + return distributionSetPrefix; + } + } + + private static class EventHandlerMock { + private final List events = Collections + .synchronizedList(new LinkedList()); + private final CountDownLatch latch; + private final int expectedNumberOfEvents; + + private EventHandlerMock(final int expectedNumberOfEvents) { + this.expectedNumberOfEvents = expectedNumberOfEvents; + this.latch = new CountDownLatch(expectedNumberOfEvents); + } + + @Subscribe + public void handleEvent(final TargetAssignDistributionSetEvent event) { + events.add(event); + latch.countDown(); + } + + public List getEvents(final long timeout, final TimeUnit unit) + throws InterruptedException { + latch.await(timeout, unit); + final List handledEvents = new LinkedList( + events); + assertThat(handledEvents).hasSize(expectedNumberOfEvents) + .as("Did not receive the expected amount of events (" + expectedNumberOfEvents + + ") within timeout. Received events are " + handledEvents); + return handledEvents; + } + } + + private static class CancelEventHandlerMock { + private final List events = Collections + .synchronizedList(new LinkedList()); + private final CountDownLatch latch; + private final int expectedNumberOfEvents; + + private CancelEventHandlerMock(final int expectedNumberOfEvents) { + this.expectedNumberOfEvents = expectedNumberOfEvents; + this.latch = new CountDownLatch(expectedNumberOfEvents); + } + + @Subscribe + public void handleEvent(final CancelTargetAssignmentEvent event) { + events.add(event); + latch.countDown(); + } + + public List getEvents(final long timeout, final TimeUnit unit) + throws InterruptedException { + latch.await(timeout, unit); + final List handledEvents = new LinkedList(events); + assertThat(handledEvents).hasSize(expectedNumberOfEvents) + .as("Did not receive the expected amount of events (" + expectedNumberOfEvents + + ") within timeout. Received events are " + handledEvents); + return handledEvents; + } + } + +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/ReportManagementTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/ReportManagementTest.java new file mode 100755 index 000000000..5c9c86aa7 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/ReportManagementTest.java @@ -0,0 +1,583 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.api.Assertions.fail; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.WithSpringAuthorityRule; +import org.eclipse.hawkbit.WithUser; +import org.eclipse.hawkbit.report.model.DataReportSeries; +import org.eclipse.hawkbit.report.model.DataReportSeriesItem; +import org.eclipse.hawkbit.report.model.InnerOuterDataReportSeries; +import org.eclipse.hawkbit.report.model.SeriesTime; +import org.eclipse.hawkbit.repository.ReportManagement.DateTypes; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetInfo; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.junit.After; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.auditing.AuditingHandler; +import org.springframework.data.auditing.CurrentDateTimeProvider; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +import com.google.common.collect.Lists; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +@Features("Component Tests - Repository") +@Stories("Report Management") +public class ReportManagementTest extends AbstractIntegrationTest { + + @Autowired + private ReportManagement reportManagement; + + @Autowired + private AuditingHandler auditingHandler; + + @After + public void afterTest() { + auditingHandler.setDateTimeProvider(CurrentDateTimeProvider.INSTANCE); + } + + @Test + @Description("Tests correct statistics calculation including a correct cache evict.") + public void targetsCreatedOverPeriod() { + + // the maximum months going back from now + // create more targets than asking the report so we can check if the + // report is returning the + // correct timeframe + final int maxMonthBackAmountCreateTargets = 10; + final int maxMonthBackAmountReportTargets = 4; + + final DynamicDateTimeProvider dynamicDateTimeProvider = new DynamicDateTimeProvider(); + auditingHandler.setDateTimeProvider(dynamicDateTimeProvider); + + for (int month = 0; month < maxMonthBackAmountCreateTargets; month++) { + dynamicDateTimeProvider.nowMinusMonths(month); + targetManagement.createTarget(new Target("t" + month)); + } + + final LocalDateTime to = LocalDateTime.now(); + final LocalDateTime from = to.minusMonths(maxMonthBackAmountReportTargets); + + DataReportSeries targetsCreatedOverPeriod = reportManagement + .targetsCreatedOverPeriod(DateTypes.perMonth(), from, to); + + // +1 because we go back #maxMonthBackAmountReportTargets but in the + // report the current month + // is included for sure, so from this month we go back + assertThat(targetsCreatedOverPeriod.getData()).hasSize(maxMonthBackAmountReportTargets + 1); + for (final DataReportSeriesItem reportItem : targetsCreatedOverPeriod.getData()) { + // only one target is created for each month + assertThat(reportItem.getData().intValue()).isEqualTo(1); + } + + // check cache evict + for (int month = 0; month < maxMonthBackAmountCreateTargets; month++) { + dynamicDateTimeProvider.nowMinusMonths(month); + targetManagement.createTarget(new Target("t2" + month)); + } + targetsCreatedOverPeriod = reportManagement.targetsCreatedOverPeriod(DateTypes.perMonth(), from, to); + for (final DataReportSeriesItem reportItem : targetsCreatedOverPeriod.getData()) { + assertThat(reportItem.getData().intValue()).isEqualTo(2); + } + } + + @Test + @Description("Tests correct statistics calculation including a correct cache evict.") + public void targetsFeedbackOverPeriod() { + + // the maximum months going back from now + // create more targets than asking the report so we can check if the + // report is returning the + // correct timeframe + final int maxMonthBackAmountCreateTargets = 10; + final int maxMonthBackAmountReportTargets = 4; + + final LocalDateTime to = LocalDateTime.now(); + final LocalDateTime from = to.minusMonths(maxMonthBackAmountReportTargets); + + final DistributionSet distributionSet = TestDataUtil.generateDistributionSet("ds", softwareManagement, + distributionSetManagement); + + final DynamicDateTimeProvider dynamicDateTimeProvider = new DynamicDateTimeProvider(); + auditingHandler.setDateTimeProvider(dynamicDateTimeProvider); + + for (int month = 0; month < maxMonthBackAmountCreateTargets; month++) { + dynamicDateTimeProvider.nowMinusMonths(month); + final Target createTarget = targetManagement.createTarget(new Target("t" + month)); + final DistributionSetAssignmentResult result = deploymentManagement.assignDistributionSet(distributionSet, + Lists.newArrayList(createTarget)); + controllerManagament.registerRetrieved(result.getActions().get(0), + "Controller retrieved update action and should start now the download."); + } + DataReportSeries feedbackReceivedOverTime = reportManagement + .feedbackReceivedOverTime(DateTypes.perMonth(), from, to); + // +1 because we go back #maxMonthBackAmountReportTargets but in the + // report the current month + // is included for sure, so from this month we go back + assertThat(feedbackReceivedOverTime.getData()).hasSize(maxMonthBackAmountReportTargets + 1); + for (final DataReportSeriesItem reportItem : feedbackReceivedOverTime.getData()) { + // only one target feedback is created for each month + assertThat(reportItem.getData().intValue()).isEqualTo(1); + } + + // check cache evict + for (int month = 0; month < maxMonthBackAmountCreateTargets; month++) { + dynamicDateTimeProvider.nowMinusMonths(month); + final Target createTarget = targetManagement.createTarget(new Target("t2" + month)); + final DistributionSetAssignmentResult result = deploymentManagement.assignDistributionSet(distributionSet, + Lists.newArrayList(createTarget)); + controllerManagament.registerRetrieved(result.getActions().get(0), + "Controller retrieved update action and should start now the download."); + } + feedbackReceivedOverTime = reportManagement.feedbackReceivedOverTime(DateTypes.perMonth(), from, to); + for (final DataReportSeriesItem reportItem : feedbackReceivedOverTime.getData()) { + assertThat(reportItem.getData().intValue()).isEqualTo(2); + } + + } + + @Test + @Description("Tests correct statistics calculation including a correct cache evict.") + public void distributionUsageInstalled() { + final Target knownTarget1 = targetManagement.createTarget(new Target("t1")); + final Target knownTarget2 = targetManagement.createTarget(new Target("t2")); + final Target knownTarget3 = targetManagement.createTarget(new Target("t3")); + final Target knownTarget4 = targetManagement.createTarget(new Target("t4")); + final Target knownTarget5 = targetManagement.createTarget(new Target("t5")); + + final SoftwareModule ah = softwareManagement + .createSoftwareModule(new SoftwareModule(appType, "agent-hub", "1.0.1", null, "")); + final SoftwareModule jvm = softwareManagement + .createSoftwareModule(new SoftwareModule(runtimeType, "oracle-jre", "1.7.2", null, "")); + final SoftwareModule os = softwareManagement + .createSoftwareModule(new SoftwareModule(osType, "poky", "3.0.2", null, "")); + + final DistributionSet distributionSet1 = distributionSetManagement + .createDistributionSet(TestDataUtil.buildDistributionSet("ds1", "0.0.0", standardDsType, os, jvm, ah)); + final DistributionSet distributionSet11 = distributionSetManagement + .createDistributionSet(TestDataUtil.buildDistributionSet("ds1", "0.0.1", standardDsType, os, jvm, ah)); + final DistributionSet distributionSet2 = distributionSetManagement + .createDistributionSet(TestDataUtil.buildDistributionSet("ds2", "0.0.2", standardDsType, os, jvm, ah)); + final DistributionSet distributionSet3 = distributionSetManagement + .createDistributionSet(TestDataUtil.buildDistributionSet("ds3", "0.0.3", standardDsType, os, jvm, ah)); + + // ds1(0.0.0)=[target1,target2], ds1(0.0.1)=[target3] + deploymentManagement.assignDistributionSet(distributionSet1.getId(), knownTarget1.getControllerId()); + deploymentManagement.assignDistributionSet(distributionSet1.getId(), knownTarget2.getControllerId()); + deploymentManagement.assignDistributionSet(distributionSet11.getId(), knownTarget3.getControllerId()); + + // ds2=[target4] + deploymentManagement.assignDistributionSet(distributionSet2.getId(), knownTarget4.getControllerId()); + + // ds3=[target5] --> ONLY ASSIGNED AND NOT INSTALLED + deploymentManagement.assignDistributionSet(distributionSet3.getId(), knownTarget5.getControllerId()); + + // set installed status + sendUpdateActionStatusToTargets(distributionSet1, Lists.newArrayList(knownTarget1, knownTarget2), + Status.FINISHED, "some message"); + sendUpdateActionStatusToTargets(distributionSet11, Lists.newArrayList(knownTarget3), Status.FINISHED, + "some message"); + sendUpdateActionStatusToTargets(distributionSet2, Lists.newArrayList(knownTarget4), Status.FINISHED, + "some message"); + + List> distributionUsage = reportManagement.distributionUsageInstalled(100); + + for (final InnerOuterDataReportSeries innerOuterDataReportSeries : distributionUsage) { + + // innerseries only have one data of the name and the total count + final DataReportSeriesItem dataReportSeriesItem = innerOuterDataReportSeries.getInnerSeries() + .getData()[0]; + if (dataReportSeriesItem.getType().equals("ds1")) { + // total count of three because ds1 has two different versions + assertThat(dataReportSeriesItem.getData()).isEqualTo(3L); + final DataReportSeriesItem[] outerData = innerOuterDataReportSeries.getOuterSeries().getData(); + assertThat(Arrays.stream(outerData).map(DataReportSeriesItem::getType).collect(Collectors.toList())) + .contains("0.0.0", "0.0.1"); + } else if (dataReportSeriesItem.getType().equals("ds2")) { + assertThat(dataReportSeriesItem.getData()).isEqualTo(1L); + final DataReportSeriesItem[] outerData = innerOuterDataReportSeries.getOuterSeries().getData(); + assertThat(outerData).hasSize(1); + assertThat(outerData[0].getType()).isEqualTo("0.0.2"); + + } else if (dataReportSeriesItem.getType().equals("ds3")) { + assertThat(dataReportSeriesItem.getData()).isEqualTo(0L); + final DataReportSeriesItem[] outerData = innerOuterDataReportSeries.getOuterSeries().getData(); + assertThat(outerData).hasSize(1); + assertThat(outerData[0].getType()).isEqualTo("0.0.3"); + } else { + fail("no assertion count for distribution set " + dataReportSeriesItem.getType()); + } + } + + // Test cache evict + final Target knownTarget6 = targetManagement.createTarget(new Target("t6")); + deploymentManagement.assignDistributionSet(distributionSet1.getId(), knownTarget6.getControllerId()); + sendUpdateActionStatusToTargets(distributionSet1, Lists.newArrayList(knownTarget6), Status.FINISHED, + "some message"); + distributionUsage = reportManagement.distributionUsageInstalled(100); + for (final InnerOuterDataReportSeries innerOuterDataReportSeries : distributionUsage) { + final DataReportSeriesItem dataReportSeriesItem = innerOuterDataReportSeries.getInnerSeries() + .getData()[0]; + if (dataReportSeriesItem.getType().equals("ds1")) { + assertThat(dataReportSeriesItem.getData()).isEqualTo(4L); + + } + } + } + + @Test + @Description("Tests correct statistics calculation including a correct cache evict.") + public void targetStatusReport() { + + final long knownErrorCount = 5; + final long knownSyncCount = 4; + final long knownPendingCount = 3; + final long knownRegCount = 2; + final long knownUnknownCount = 1; + + createTargetsWithStatus("error", knownErrorCount, TargetUpdateStatus.ERROR); + createTargetsWithStatus("snyc", knownSyncCount, TargetUpdateStatus.IN_SYNC); + createTargetsWithStatus("pending", knownPendingCount, TargetUpdateStatus.PENDING); + createTargetsWithStatus("reg", knownRegCount, TargetUpdateStatus.REGISTERED); + createTargetsWithStatus("unknown", knownUnknownCount, TargetUpdateStatus.UNKNOWN); + + DataReportSeries targetStatus = reportManagement.targetStatus(); + for (final DataReportSeriesItem reportItem : targetStatus.getData()) { + + switch (reportItem.getType()) { + case ERROR: + assertThat(reportItem.getData()).isEqualTo(knownErrorCount); + break; + case IN_SYNC: + assertThat(reportItem.getData()).isEqualTo(knownSyncCount); + break; + case PENDING: + assertThat(reportItem.getData()).isEqualTo(knownPendingCount); + break; + case REGISTERED: + assertThat(reportItem.getData()).isEqualTo(knownRegCount); + break; + case UNKNOWN: + assertThat(reportItem.getData()).isEqualTo(knownUnknownCount); + break; + default: + fail("missing case for unknown target update status " + reportItem.getType()); + } + } + + // test cache evict + createTargetsWithStatus("error2", knownErrorCount, TargetUpdateStatus.ERROR); + createTargetsWithStatus("snyc2", knownSyncCount, TargetUpdateStatus.IN_SYNC); + createTargetsWithStatus("pending2", knownPendingCount, TargetUpdateStatus.PENDING); + createTargetsWithStatus("reg2", knownRegCount, TargetUpdateStatus.REGISTERED); + createTargetsWithStatus("unknown2", knownUnknownCount, TargetUpdateStatus.UNKNOWN); + + targetStatus = reportManagement.targetStatus(); + for (final DataReportSeriesItem reportItem : targetStatus.getData()) { + + switch (reportItem.getType()) { + case ERROR: + assertThat(reportItem.getData()).isEqualTo(knownErrorCount * 2); + break; + case IN_SYNC: + assertThat(reportItem.getData()).isEqualTo(knownSyncCount * 2); + break; + case PENDING: + assertThat(reportItem.getData()).isEqualTo(knownPendingCount * 2); + break; + case REGISTERED: + assertThat(reportItem.getData()).isEqualTo(knownRegCount * 2); + break; + case UNKNOWN: + assertThat(reportItem.getData()).isEqualTo(knownUnknownCount * 2); + break; + default: + fail("missing case for unknown target update status " + reportItem.getType()); + } + } + } + + @Test + @Description("Tests correct statistics calculation including a correct cache evict.") + public void topXDistributionUsage() { + + final Target knownTarget1 = targetManagement.createTarget(new Target("t1")); + final Target knownTarget2 = targetManagement.createTarget(new Target("t2")); + final Target knownTarget3 = targetManagement.createTarget(new Target("t3")); + final Target knownTarget4 = targetManagement.createTarget(new Target("t4")); + + final SoftwareModule ah = softwareManagement + .createSoftwareModule(new SoftwareModule(appType, "agent-hub", "1.0.1", null, "")); + final SoftwareModule jvm = softwareManagement + .createSoftwareModule(new SoftwareModule(runtimeType, "oracle-jre", "1.7.2", null, "")); + final SoftwareModule os = softwareManagement + .createSoftwareModule(new SoftwareModule(osType, "poky", "3.0.2", null, "")); + + final DistributionSet distributionSet1 = distributionSetManagement + .createDistributionSet(TestDataUtil.buildDistributionSet("ds1", "0.0.0", standardDsType, os, jvm, ah)); + final DistributionSet distributionSet11 = distributionSetManagement + .createDistributionSet(TestDataUtil.buildDistributionSet("ds1", "0.0.1", standardDsType, os, jvm, ah)); + final DistributionSet distributionSet2 = distributionSetManagement + .createDistributionSet(TestDataUtil.buildDistributionSet("ds2", "0.0.2", standardDsType, os, jvm, ah)); + final DistributionSet distributionSet3 = distributionSetManagement + .createDistributionSet(TestDataUtil.buildDistributionSet("ds3", "0.0.3", standardDsType, os, jvm, ah)); + + // ds1(0.0.0)=[target1,target2], ds1(0.0.1)=[target3] + deploymentManagement.assignDistributionSet(distributionSet1.getId(), knownTarget1.getControllerId()); + deploymentManagement.assignDistributionSet(distributionSet1.getId(), knownTarget2.getControllerId()); + deploymentManagement.assignDistributionSet(distributionSet11.getId(), knownTarget3.getControllerId()); + + // ds2=[target4] + deploymentManagement.assignDistributionSet(distributionSet2.getId(), knownTarget4.getControllerId()); + + // expect: ds1(0.0.0)=[target1,target2], ds1(0.0.1)=[target3], + // ds2=[target4], ds3=[] + + List> distributionUsage = reportManagement.distributionUsageAssigned(100); + + for (final InnerOuterDataReportSeries innerOuterDataReportSeries : distributionUsage) { + + // innerseries only have one data of the name and the total count + final DataReportSeriesItem dataReportSeriesItem = innerOuterDataReportSeries.getInnerSeries() + .getData()[0]; + if (dataReportSeriesItem.getType().equals("ds1")) { + // total count of three because ds1 has two different versions + assertThat(dataReportSeriesItem.getData()).isEqualTo(3L); + final DataReportSeriesItem[] outerData = innerOuterDataReportSeries.getOuterSeries().getData(); + assertThat(Arrays.stream(outerData).map(DataReportSeriesItem::getType).collect(Collectors.toList())) + .contains("0.0.0", "0.0.1"); + } else if (dataReportSeriesItem.getType().equals("ds2")) { + assertThat(dataReportSeriesItem.getData()).isEqualTo(1L); + final DataReportSeriesItem[] outerData = innerOuterDataReportSeries.getOuterSeries().getData(); + assertThat(outerData).hasSize(1); + assertThat(outerData[0].getType()).isEqualTo("0.0.2"); + + } else if (dataReportSeriesItem.getType().equals("ds3")) { + assertThat(dataReportSeriesItem.getData()).isEqualTo(0L); + final DataReportSeriesItem[] outerData = innerOuterDataReportSeries.getOuterSeries().getData(); + assertThat(outerData).hasSize(1); + assertThat(outerData[0].getType()).isEqualTo("0.0.3"); + } else { + fail("no assertion count for distribution set " + dataReportSeriesItem.getType()); + } + } + + // test cache evict + final Target knownTarget5 = targetManagement.createTarget(new Target("t5")); + deploymentManagement.assignDistributionSet(distributionSet1.getId(), knownTarget5.getControllerId()); + distributionUsage = reportManagement.distributionUsageAssigned(100); + for (final InnerOuterDataReportSeries innerOuterDataReportSeries : distributionUsage) { + final DataReportSeriesItem dataReportSeriesItem = innerOuterDataReportSeries.getInnerSeries() + .getData()[0]; + if (dataReportSeriesItem.getType().equals("ds1")) { + assertThat(dataReportSeriesItem.getData()).isEqualTo(4L); + } + } + } + + @Test + @Description("Tests correct statistics calculation including a correct cache evict.") + public void lastPollTargets() { + // --- prepare --- + final LocalDateTime now = LocalDateTime.now(); + final int knownTargetsPollLastHour = 2; + final int knownTargetsPollLastDay = 3; + final int knownTargetsPollLastWeek = 4; + final int knownTargetsPollLastMonth = 5; + final int knownTargetsPollLastYear = 6; + final int knownTargetsNeverPoll = 7; + // never + createTargets("neverPoll", knownTargetsNeverPoll, null); + // hour + createTargets("hourPoll", knownTargetsPollLastHour, now.minusMinutes(59)); + // day + createTargets("dayPoll", knownTargetsPollLastDay, now.minusHours(23)); + // week + createTargets("weekPoll", knownTargetsPollLastWeek, now.minusDays(6)); + // month + createTargets("monthPoll", knownTargetsPollLastMonth, now.minusWeeks(3)); + // year + createTargets("yearPoll", knownTargetsPollLastYear, now.minusMonths(11)); + + // --- Test --- + DataReportSeries targetsNotLastPoll = reportManagement.targetsLastPoll(); + DataReportSeriesItem[] data = targetsNotLastPoll.getData(); + + // for( final DataReportSeriesItem dataReportSeriesItem : + // data ) { + // System.out.println( dataReportSeriesItem.getData() ); + // } + + // --- Verfiy --- + + // verify hour + assertThat(data[0].getType()).isEqualTo(SeriesTime.HOUR); + assertThat(data[0].getData()).isEqualTo((long) knownTargetsPollLastHour); + // verify day + assertThat(data[1].getType()).isEqualTo(SeriesTime.DAY); + assertThat(data[1].getData()).isEqualTo((long) knownTargetsPollLastDay); + // verify week + assertThat(data[2].getType()).isEqualTo(SeriesTime.WEEK); + assertThat(data[2].getData()).isEqualTo((long) knownTargetsPollLastWeek); + + // test cache evict + createTargets("hourPoll2", knownTargetsPollLastHour, now.minusMinutes(59)); + targetsNotLastPoll = reportManagement.targetsLastPoll(); + data = targetsNotLastPoll.getData(); + assertThat(data[0].getType()).isEqualTo(SeriesTime.HOUR); + assertThat(data[0].getData()).isEqualTo((long) knownTargetsPollLastHour * 2); + + } + + @Test + @WithUser(tenantId = "mytenant", allSpPermissions = true) + @Description("Ensures that targets created report is tenant aware and only creates a report for the current tenant.") + public void targetsCreatedOverPeriodMultiTenancyAware() throws Exception { + final int targetCreateAmount = 10; + + // create targets for another tenant + securityRule.runAs(WithSpringAuthorityRule.withUserAndTenant("user", "anotherTenant"), new Callable() { + @Override + public Void call() throws Exception { + for (int index = 0; index < targetCreateAmount; index++) { + targetManagement.createTarget(new Target("t" + index)); + } + return null; + } + }); + + // ensure targets has been created for 'anotherTenant' + final Slice targetsForAnotherTenant = securityRule.runAs( + WithSpringAuthorityRule.withUserAndTenant("user", "anotherTenant"), new Callable>() { + @Override + public Slice call() throws Exception { + return targetManagement.findTargetsAll(new PageRequest(0, 1000)); + } + }); + assertThat(targetsForAnotherTenant).hasSize(targetCreateAmount); + + final LocalDateTime to = LocalDateTime.now(); + final LocalDateTime from = to.minusMonths(targetCreateAmount); + // now retrieve the report for the 'mytenant' + final DataReportSeries targetsCreatedOverPeriod = reportManagement + .targetsCreatedOverPeriod(DateTypes.perMonth(), from, to); + // final no targets should final be created for this tenant + assertThat(targetsCreatedOverPeriod.getData()).hasSize(0); + + } + + private void createTargets(final String prefix, final int amount, final LocalDateTime lastTargetQuery) { + for (int index = 0; index < amount; index++) { + final Target target = new Target(prefix + index); + final Target createTarget = targetManagement.createTarget(target); + if (lastTargetQuery != null) { + final TargetInfo targetInfo = createTarget.getTargetInfo(); + targetInfo.setNew(false); + targetInfo + .setLastTargetQuery(lastTargetQuery.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); + targetInfoRepository.save(targetInfo); + } + } + } + + private void createTargetsWithStatus(final String prefix, final long amount, final TargetUpdateStatus status) { + for (int index = 0; index < amount; index++) { + final Target target = new Target(prefix + index); + final Target sTarget = targetRepository.save(target); + final TargetInfo targetInfo = sTarget.getTargetInfo(); + targetInfo.setUpdateStatus(status); + targetInfoRepository.save(targetInfo); + } + } + + private List sendUpdateActionStatusToTargets(final DistributionSet dsA, final Iterable targs, + final Status status, final String... msgs) { + final List result = new ArrayList(); + for (final Target t : targs) { + final List findByTarget = actionRepository.findByTarget(t); + for (final Action action : findByTarget) { + result.add(sendUpdateActionStatusToTarget(status, action, t, msgs)); + } + } + return result; + } + + private Target sendUpdateActionStatusToTarget(final Status status, final Action updActA, final Target t, + final String... msgs) { + updActA.setStatus(status); + + final ActionStatus statusMessages = new ActionStatus(); + statusMessages.setAction(updActA); + statusMessages.setOccurredAt(System.currentTimeMillis()); + statusMessages.setStatus(status); + for (final String msg : msgs) { + statusMessages.addMessage(msg); + } + controllerManagament.addUpdateActionStatus(statusMessages, updActA); + return targetManagement.findTargetByControllerID(t.getControllerId()); + } + + private class DynamicDateTimeProvider implements DateTimeProvider { + + private Calendar datetime = Calendar.getInstance(); + + /* + * (non-Javadoc) + * + * @see org.springframework.data.auditing.DateTimeProvider#getNow() + */ + @Override + public Calendar getNow() { + return datetime; + } + + public void now() { + datetime = Calendar.getInstance(); + } + + public void nowMinusMonths(final int amount) { + datetime = Calendar.getInstance(); + datetime.add(Calendar.MONTH, -amount); + } + + /** + * @param datetime + * the datetime to set + */ + public void setDatetime(final Calendar datetime) { + this.datetime = datetime; + } + } +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/SoftwareManagementForDSTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/SoftwareManagementForDSTest.java new file mode 100755 index 000000000..30f4a8cb0 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/SoftwareManagementForDSTest.java @@ -0,0 +1,794 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.WithUser; +import org.eclipse.hawkbit.repository.DistributionSetFilter.DistributionSetFilterBuilder; +import org.eclipse.hawkbit.repository.exception.DistributionSetTypeUndefinedException; +import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.exception.EntityLockedException; +import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; +import org.eclipse.hawkbit.repository.exception.UnsupportedSoftwareModuleForThisDistributionSetException; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; +import org.eclipse.hawkbit.repository.model.DistributionSetTag; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.Target; +import org.junit.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +import com.google.common.collect.Lists; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * {@link SoftwareManagement} test focused on {@link DistributionSet} and + * {@link DistributionSetType} related stuff. + * + * + * + */ +@Features("Component Tests - Repository") +@Stories("Software Management") +public class SoftwareManagementForDSTest extends AbstractIntegrationTest { + + @Test + @Description("Tests the successfull module update of unused distribution set type which is in fact allowed.") + public void updateUnassignedDistributionSetTypeModules() { + DistributionSetType updatableType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("updatableType", "to be deleted", "")); + assertThat(distributionSetManagement.findDistributionSetTypeByKey("updatableType").getMandatoryModuleTypes()) + .isEmpty(); + + // add OS + updatableType.addMandatoryModuleType(osType); + updatableType = distributionSetManagement.updateDistributionSetType(updatableType); + assertThat(distributionSetManagement.findDistributionSetTypeByKey("updatableType").getMandatoryModuleTypes()) + .containsOnly(osType); + + // add JVM + updatableType.addMandatoryModuleType(runtimeType); + updatableType = distributionSetManagement.updateDistributionSetType(updatableType); + assertThat(distributionSetManagement.findDistributionSetTypeByKey("updatableType").getMandatoryModuleTypes()) + .containsOnly(osType, runtimeType); + + // remove OS + updatableType.removeModuleType(osType.getId()); + updatableType = distributionSetManagement.updateDistributionSetType(updatableType); + assertThat(distributionSetManagement.findDistributionSetTypeByKey("updatableType").getMandatoryModuleTypes()) + .containsOnly(runtimeType); + } + + @Test + @Description("Tests the successfull update of used distribution set type meta data hich is in fact allowed.") + public void updateAssignedDistributionSetTypeMetaData() { + final DistributionSetType nonUpdatableType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("updatableType", "to be deletd", "")); + assertThat(distributionSetManagement.findDistributionSetTypeByKey("updatableType").getMandatoryModuleTypes()) + .isEmpty(); + distributionSetManagement + .createDistributionSet(new DistributionSet("newtypesoft", "1", "", nonUpdatableType, null)); + + nonUpdatableType.setDescription("a new description"); + nonUpdatableType.setColour("test123"); + + distributionSetManagement.updateDistributionSetType(nonUpdatableType); + + assertThat(distributionSetManagement.findDistributionSetTypeByKey("updatableType").getDescription()) + .isEqualTo("a new description"); + assertThat(distributionSetManagement.findDistributionSetTypeByKey("updatableType").getColour()) + .isEqualTo("test123"); + } + + @Test(expected = EntityReadOnlyException.class) + @Description("Tests the unsuccessfull update of used distribution set type (module addition).") + public void addModuleToAssignedDistributionSetTypeFails() { + final DistributionSetType nonUpdatableType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("updatableType", "to be deletd", "")); + assertThat(distributionSetManagement.findDistributionSetTypeByKey("updatableType").getMandatoryModuleTypes()) + .isEmpty(); + distributionSetManagement + .createDistributionSet(new DistributionSet("newtypesoft", "1", "", nonUpdatableType, null)); + + nonUpdatableType.addMandatoryModuleType(osType); + distributionSetManagement.updateDistributionSetType(nonUpdatableType); + } + + @Test(expected = EntityReadOnlyException.class) + @Description("Tests the unsuccessfull update of used distribution set type (module removal).") + public void removeModuleToAssignedDistributionSetTypeFails() { + DistributionSetType nonUpdatableType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("updatableType", "to be deletd", "")); + assertThat(distributionSetManagement.findDistributionSetTypeByKey("updatableType").getMandatoryModuleTypes()) + .isEmpty(); + + nonUpdatableType.addMandatoryModuleType(osType); + nonUpdatableType = distributionSetManagement.updateDistributionSetType(nonUpdatableType); + distributionSetManagement + .createDistributionSet(new DistributionSet("newtypesoft", "1", "", nonUpdatableType, null)); + + nonUpdatableType.removeModuleType(osType.getId()); + nonUpdatableType = distributionSetManagement.updateDistributionSetType(nonUpdatableType); + } + + @Test + @Description("Tests the successfull deletion of unused (hard delete) distribution set types.") + public void deleteUnassignedDistributionSetType() { + final DistributionSetType hardDelete = distributionSetManagement + .createDistributionSetType(new DistributionSetType("deleted", "to be deleted", "")); + + assertThat(distributionSetTypeRepository.findAll()).contains(hardDelete); + distributionSetManagement.deleteDistributionSetType(hardDelete); + + assertThat(distributionSetTypeRepository.findAll()).doesNotContain(hardDelete); + } + + @Test + @Description("Tests the successfull deletion of used (soft delete) distribution set types.") + public void deleteAssignedDistributionSetType() { + final DistributionSetType softDelete = distributionSetManagement + .createDistributionSetType(new DistributionSetType("softdeleted", "to be deletd", "")); + + assertThat(distributionSetTypeRepository.findAll()).contains(softDelete); + final DistributionSet dsNewType = distributionSetManagement + .createDistributionSet(new DistributionSet("newtypesoft", "1", "", softDelete, null)); + + distributionSetManagement.deleteDistributionSetType(softDelete); + assertThat(distributionSetManagement.findDistributionSetTypeByKey("softdeleted").isDeleted()).isEqualTo(true); + } + + // TODO: kzimmerm: test N+1 + + @Test(expected = EntityAlreadyExistsException.class) + @Description("Ensures that it is not possible to create a DS that already exists (unique constraint is on name,version for DS).") + public void createDuplicateDistributionSetsFailsWithException() { + TestDataUtil.generateDistributionSet("a", softwareManagement, distributionSetManagement); + + TestDataUtil.generateDistributionSet("a", softwareManagement, distributionSetManagement); + + } + + @Test + @Description("Checks that metadata for a distribution set can be created.") + public void createDistributionSetMetadata() { + final String knownKey = "dsMetaKnownKey"; + final String knownValue = "dsMetaKnownValue"; + + final DistributionSet ds = TestDataUtil.generateDistributionSet("testDs", softwareManagement, + distributionSetManagement); + + final DistributionSetMetadata metadata = new DistributionSetMetadata(knownKey, ds, knownValue); + final DistributionSetMetadata createdMetadata = distributionSetManagement + .createDistributionSetMetadata(metadata); + + assertThat(createdMetadata).isNotNull(); + assertThat(createdMetadata.getId().getKey()).isEqualTo(knownKey); + assertThat(createdMetadata.getDistributionSet().getId()).isEqualTo(ds.getId()); + assertThat(createdMetadata.getValue()).isEqualTo(knownValue); + } + + @Test + @Description("Ensures that updates concerning the internal software structure of a DS are not possible if the DS is already assigned.") + public void updateDistributionSetForbiddedWithIllegalUpdate() { + // prepare data + Target target = new Target("4711"); + target = targetManagement.createTarget(target); + + SoftwareModule ah2 = new SoftwareModule(appType, "agent-hub2", "1.0.5", null, ""); + SoftwareModule os2 = new SoftwareModule(osType, "poky2", "3.0.3", null, ""); + + DistributionSet ds = TestDataUtil.generateDistributionSet("ds-1", softwareManagement, + distributionSetManagement); + + ah2 = softwareManagement.createSoftwareModule(ah2); + os2 = softwareManagement.createSoftwareModule(os2); + + // update is allowed as it is still not assigned to a target + ds.addModule(ah2); + ds = distributionSetManagement.updateDistributionSet(ds); + + // assign target + deploymentManagement.assignDistributionSet(ds.getId(), target.getControllerId()); + ds = distributionSetManagement.findDistributionSetByIdWithDetails(ds.getId()); + + // description change is still allowed + ds.setDescription("a different desc"); + ds = distributionSetManagement.updateDistributionSet(ds); + + // description change is still allowed + ds.setName("a new name"); + ds = distributionSetManagement.updateDistributionSet(ds); + + // description change is still allowed + ds.setVersion("a new version"); + ds = distributionSetManagement.updateDistributionSet(ds); + + // not allowed as it is assigned now + ds.addModule(os2); + try { + ds = distributionSetManagement.updateDistributionSet(ds); + fail("Expected EntityLockedException"); + } catch (final EntityLockedException e) { + + } + + // not allowed as it is assigned now + ds.removeModule(ds.findFirstModuleByType(appType)); + try { + ds = distributionSetManagement.updateDistributionSet(ds); + fail("Expected EntityLockedException"); + } catch (final EntityLockedException e) { + + } + } + + @Test(expected = DistributionSetTypeUndefinedException.class) + @Description("Ensures that it is not possible to add a software module to a set that has no type defined.") + public void updateDistributionSetModuleWithUndefinedTypeFails() { + final DistributionSet testSet = new DistributionSet(); + final SoftwareModule module = new SoftwareModule(appType, "agent-hub2", "1.0.5", null, ""); + + // update data + testSet.addModule(module); + } + + @Test(expected = UnsupportedSoftwareModuleForThisDistributionSetException.class) + @Description("Ensures that it is not possible to add a software module that is not defined of the DS's type.") + public void updateDistributionSetUnsupportedModuleFails() { + final DistributionSet set = new DistributionSet("agent-hub2", "1.0.5", "desc", + new DistributionSetType("test", "test", "test").addMandatoryModuleType(osType), null); + final SoftwareModule module = new SoftwareModule(appType, "agent-hub2", "1.0.5", null, ""); + + // update data + set.addModule(module); + } + + @Test + @Description("Legal updates of a DS, e.g. name or description and module addition, removal while still unassigned.") + public void updateDistributionSet() { + // prepare data + Target target = new Target("4711"); + target = targetManagement.createTarget(target); + + SoftwareModule os2 = new SoftwareModule(osType, "poky2", "3.0.3", null, ""); + final SoftwareModule app2 = new SoftwareModule(appType, "app2", "3.0.3", null, ""); + + DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, distributionSetManagement); + + os2 = softwareManagement.createSoftwareModule(os2); + + // update data + // legal update of module addition + ds.addModule(os2); + distributionSetManagement.updateDistributionSet(ds); + ds = distributionSetManagement.findDistributionSetByIdWithDetails(ds.getId()); + assertThat(ds.findFirstModuleByType(osType)).isEqualTo(os2); + + // legal update of module removal + ds.removeModule(ds.findFirstModuleByType(appType)); + distributionSetManagement.updateDistributionSet(ds); + ds = distributionSetManagement.findDistributionSetByIdWithDetails(ds.getId()); + assertThat(ds.findFirstModuleByType(appType)).isNull(); + + // Update description + ds.setDescription("a new description"); + distributionSetManagement.updateDistributionSet(ds); + ds = distributionSetManagement.findDistributionSetByIdWithDetails(ds.getId()); + assertThat(ds.getDescription()).isEqualTo("a new description"); + + // Update name + ds.setName("a new name"); + distributionSetManagement.updateDistributionSet(ds); + ds = distributionSetManagement.findDistributionSetByIdWithDetails(ds.getId()); + assertThat(ds.getName()).isEqualTo("a new name"); + } + + @Test + @WithUser(allSpPermissions = true) + @Description("Checks that metadata for a distribution set can be updated.") + public void updateDistributionSetMetadata() throws InterruptedException { + final String knownKey = "myKnownKey"; + final String knownValue = "myKnownValue"; + final String knownUpdateValue = "myNewUpdatedValue"; + + // create a DS + final DistributionSet ds = TestDataUtil.generateDistributionSet("testDs", softwareManagement, + distributionSetManagement); + // initial opt lock revision must be zero + assertThat(ds.getOptLockRevision()).isEqualTo(1L); + + // create an DS meta data entry + final DistributionSetMetadata dsMetadata = distributionSetManagement + .createDistributionSetMetadata(new DistributionSetMetadata(knownKey, ds, knownValue)); + + DistributionSet changedLockRevisionDS = distributionSetManagement.findDistributionSetById(ds.getId()); + assertThat(changedLockRevisionDS.getOptLockRevision()).isEqualTo(2L); + + // modifying the meta data value + dsMetadata.setValue(knownUpdateValue); + dsMetadata.setKey(knownKey); + dsMetadata.setDistributionSet(changedLockRevisionDS); + + Thread.sleep(100); + + // update the DS metadata + final DistributionSetMetadata updated = distributionSetManagement.updateDistributionSetMetadata(dsMetadata); + // we are updating the sw meta data so also modifying the base software + // module so opt lock + // revision must be three + changedLockRevisionDS = distributionSetManagement.findDistributionSetById(ds.getId()); + assertThat(changedLockRevisionDS.getOptLockRevision()).isEqualTo(3L); + assertThat(changedLockRevisionDS.getLastModifiedAt()).isGreaterThan(0L); + + // verify updated meta data contains the updated value + assertThat(updated).isNotNull(); + assertThat(updated.getValue()).isEqualTo(knownUpdateValue); + assertThat(updated.getId().getKey()).isEqualTo(knownKey); + assertThat(updated.getDistributionSet().getId()).isEqualTo(ds.getId()); + } + + @Test + @Description("Tests that a DS queue is possible where the result is ordered by the target assignment, i.e. assigned first in the list.") + public void findDistributionSetsAllOrderedByLinkTarget() { + + final List buildDistributionSets = TestDataUtil.generateDistributionSets("dsOrder", 10, + softwareManagement, distributionSetManagement); + + final List buildTargetFixtures = targetManagement + .createTargets(TestDataUtil.buildTargetFixtures(5, "tOrder", "someDesc")); + + final Iterator dsIterator = buildDistributionSets.iterator(); + final Iterator tIterator = buildTargetFixtures.iterator(); + final DistributionSet dsFirst = dsIterator.next(); + final DistributionSet dsSecond = dsIterator.next(); + final DistributionSet dsThree = dsIterator.next(); + final DistributionSet dsFour = dsIterator.next(); + final Target tFirst = tIterator.next(); + final Target tSecond = tIterator.next(); + + // set assigned + deploymentManagement.assignDistributionSet(dsSecond.getId(), tSecond.getControllerId()); + deploymentManagement.assignDistributionSet(dsThree.getId(), tFirst.getControllerId()); + // set installed + final ArrayList installedDSSecond = new ArrayList<>(); + installedDSSecond.add(tSecond); + sendUpdateActionStatusToTargets(dsSecond, installedDSSecond, Status.FINISHED, "some message"); + + deploymentManagement.assignDistributionSet(dsFour.getId(), tSecond.getControllerId()); + + final DistributionSetFilterBuilder distributionSetFilterBuilder = new DistributionSetFilterBuilder() + .setIsDeleted(false).setIsComplete(true).setSelectDSWithNoTag(Boolean.FALSE); + + // target first only has an assigned DS-three so check order correct + final List tFirstPin = distributionSetManagement.findDistributionSetsAllOrderedByLinkTarget( + pageReq, distributionSetFilterBuilder, tFirst.getControllerId()).getContent(); + assertThat(tFirstPin.get(0)).isEqualTo(dsThree); + assertThat(tFirstPin).hasSize(10); + + // target second has installed DS-2 and assigned DS-4 so check order + // correct + final List tSecondPin = distributionSetManagement.findDistributionSetsAllOrderedByLinkTarget( + pageReq, distributionSetFilterBuilder, tSecond.getControllerId()).getContent(); + assertThat(tSecondPin.get(0)).isEqualTo(dsSecond); + assertThat(tSecondPin.get(1)).isEqualTo(dsFour); + assertThat(tFirstPin).hasSize(10); + } + + @Test + @Description("searches for distribution sets based on the various filter options, e.g. name, version, desc., tags.") + public void searchDistributionSetsOnFilters() { + DistributionSetTag dsTagA = tagManagement + .createDistributionSetTag(new DistributionSetTag("DistributionSetTag-A")); + final DistributionSetTag dsTagB = tagManagement + .createDistributionSetTag(new DistributionSetTag("DistributionSetTag-B")); + final DistributionSetTag dsTagC = tagManagement + .createDistributionSetTag(new DistributionSetTag("DistributionSetTag-C")); + final DistributionSetTag dsTagD = tagManagement + .createDistributionSetTag(new DistributionSetTag("DistributionSetTag-D")); + + List ds100Group1 = TestDataUtil.generateDistributionSets("", 100, softwareManagement, + distributionSetManagement); + List ds100Group2 = TestDataUtil.generateDistributionSets("test2", 100, softwareManagement, + distributionSetManagement); + DistributionSet dsDeleted = TestDataUtil.generateDistributionSet("deleted", softwareManagement, + distributionSetManagement); + final DistributionSet dsInComplete = distributionSetManagement + .createDistributionSet(new DistributionSet("notcomplete", "1", "", standardDsType, null)); + + final DistributionSetType newType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("foo", "bar", "test").addMandatoryModuleType(osType) + .addOptionalModuleType(appType).addOptionalModuleType(runtimeType)); + + final DistributionSet dsNewType = distributionSetManagement + .createDistributionSet(new DistributionSet("newtype", "1", "", newType, dsDeleted.getModules())); + + deploymentManagement.assignDistributionSet(dsDeleted, + targetManagement.createTargets(Lists.newArrayList(TestDataUtil.generateTargets(5)))); + distributionSetManagement.deleteDistributionSet(dsDeleted); + dsDeleted = distributionSetManagement.findDistributionSetById(dsDeleted.getId()); + + ds100Group1 = distributionSetManagement.toggleTagAssignment(ds100Group1, dsTagA).getAssignedDs(); + dsTagA = distributionSetTagRepository.findByNameEquals(dsTagA.getName()); + ds100Group1 = distributionSetManagement.toggleTagAssignment(ds100Group1, dsTagB).getAssignedDs(); + dsTagA = distributionSetTagRepository.findByNameEquals(dsTagA.getName()); + ds100Group2 = distributionSetManagement.toggleTagAssignment(ds100Group2, dsTagA).getAssignedDs(); + dsTagA = distributionSetTagRepository.findByNameEquals(dsTagA.getName()); + + // check setup + assertThat(distributionSetRepository.findAll()).hasSize(203); + + // Find all + List expected = new ArrayList(); + expected.addAll(ds100Group1); + expected.addAll(ds100Group2); + expected.add(dsDeleted); + expected.add(dsInComplete); + expected.add(dsNewType); + + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, getDistributionSetFilterBuilder().build()).getContent()) + .hasSize(203).containsOnly(expected.toArray(new DistributionSet[0])); + + DistributionSetFilterBuilder distributionSetFilterBuilder; + + // search for not deleted + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsDeleted(Boolean.TRUE); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(1); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsDeleted(false); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()) + .hasSize(202); + + // search for completed + expected = new ArrayList(); + expected.addAll(ds100Group1); + expected.addAll(ds100Group2); + expected.add(dsDeleted); + expected.add(dsNewType); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsComplete(true); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(202) + .containsOnly(expected.toArray(new DistributionSet[0])); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsComplete(Boolean.FALSE); + expected = new ArrayList(); + expected.add(dsInComplete); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(1) + .containsOnly(expected.toArray(new DistributionSet[0])); + + // search for type + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setType(newType); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(1); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setType(standardDsType); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()) + .hasSize(202); + + // search for text + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setSearchText("%test2"); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()) + .hasSize(100); + + // search for tags + distributionSetFilterBuilder = getDistributionSetFilterBuilder() + .setTagNames(Lists.newArrayList(dsTagA.getName())); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()) + .hasSize(200); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder() + .setTagNames(Lists.newArrayList(dsTagB.getName())); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()) + .hasSize(100); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder() + .setTagNames(Lists.newArrayList(dsTagA.getName(), dsTagB.getName())); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()) + .hasSize(200); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder() + .setTagNames(Lists.newArrayList(dsTagC.getName(), dsTagB.getName())); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()) + .hasSize(100); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder() + .setTagNames(Lists.newArrayList(dsTagC.getName())); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(0); + + // combine deleted and complete + expected = new ArrayList(); + expected.addAll(ds100Group1); + expected.addAll(ds100Group2); + expected.add(dsNewType); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsComplete(Boolean.TRUE) + .setIsDeleted(Boolean.FALSE); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(201) + .containsOnly(expected.toArray(new DistributionSet[0])); + + expected = new ArrayList(); + expected.add(dsInComplete); + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsComplete(Boolean.FALSE); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(1) + .containsOnly(expected.toArray(new DistributionSet[0])); + + expected = new ArrayList(); + expected.add(dsDeleted); + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsComplete(Boolean.TRUE) + .setIsDeleted(Boolean.TRUE); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(1) + .containsOnly(expected.toArray(new DistributionSet[0])); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsDeleted(Boolean.TRUE) + .setIsComplete(Boolean.FALSE); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(0); + + // combine deleted and complete and type + expected = new ArrayList(); + expected.addAll(ds100Group1); + expected.addAll(ds100Group2); + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsDeleted(Boolean.FALSE) + .setIsComplete(Boolean.TRUE).setType(standardDsType); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(200) + .containsOnly(expected.toArray(new DistributionSet[0])); + + expected = new ArrayList(); + expected.add(dsDeleted); + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsComplete(Boolean.TRUE) + .setType(standardDsType).setIsDeleted(Boolean.TRUE); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(1) + .containsOnly(expected.toArray(new DistributionSet[0])); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsDeleted(Boolean.TRUE) + .setIsComplete(Boolean.FALSE).setType(standardDsType); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(0); + + expected = new ArrayList(); + expected.add(dsNewType); + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsComplete(Boolean.TRUE).setType(newType); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(1) + .containsOnly(expected.toArray(new DistributionSet[0])); + + // combine deleted and complete and type and text + expected = new ArrayList(); + expected.addAll(ds100Group2); + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsComplete(Boolean.TRUE) + .setType(standardDsType).setSearchText("%test2"); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(100) + .containsOnly(expected.toArray(new DistributionSet[0])); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsComplete(Boolean.TRUE) + .setIsDeleted(Boolean.TRUE).setType(standardDsType).setSearchText("%test2"); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(0); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setType(standardDsType).setSearchText("%test2") + .setIsComplete(false).setIsDeleted(false); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(0); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setType(newType).setSearchText("%test2") + .setIsComplete(Boolean.TRUE).setIsDeleted(false); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(0); + + // combine deleted and complete and type and text and tag + expected = new ArrayList(); + expected.addAll(ds100Group2); + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsComplete(true).setType(standardDsType) + .setSearchText("%test2").setTagNames(Lists.newArrayList(dsTagA.getName())); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(100) + .containsOnly(expected.toArray(new DistributionSet[0])); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setType(standardDsType).setSearchText("%test2") + .setTagNames(Lists.newArrayList(dsTagA.getName())).setIsComplete(Boolean.FALSE) + .setIsDeleted(Boolean.FALSE); + assertThat(distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getContent()).hasSize(0); + + } + + private DistributionSetFilterBuilder getDistributionSetFilterBuilder() { + return new DistributionSetFilterBuilder(); + } + + private List sendUpdateActionStatusToTargets(final DistributionSet dsA, final Iterable targs, + final Status status, final String... msgs) { + final List result = new ArrayList(); + for (final Target t : targs) { + final List findByTarget = actionRepository.findByTarget(t); + for (final Action action : findByTarget) { + result.add(sendUpdateActionStatusToTarget(status, action, t, msgs)); + } + } + return result; + } + + @Test + @Description("Simple DS load without the related data that should be loaded lazy.") + public void findDistributionSetsWithoutLazy() { + TestDataUtil.generateDistributionSets(20, softwareManagement, distributionSetManagement); + + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, false, true)).hasSize(20); + } + + @Test + @Description("Deltes a DS that is no in use. Expected behaviour is a hard delete on the database.") + public void deleteUnassignedDistributionSet() { + DistributionSet ds1 = TestDataUtil.generateDistributionSet("ds-1", softwareManagement, + distributionSetManagement); + DistributionSet ds2 = TestDataUtil.generateDistributionSet("ds-2", softwareManagement, + distributionSetManagement); + + ds1 = distributionSetManagement.findDistributionSetByNameAndVersion(ds1.getName(), ds1.getVersion()); + ds2 = distributionSetManagement.findDistributionSetByNameAndVersion(ds2.getName(), ds2.getVersion()); + + // delete a ds + assertThat(distributionSetRepository.findAll()).hasSize(2); + distributionSetManagement.deleteDistributionSet(ds1.getId()); + // not assigned so not marked as deleted but fully deleted + assertThat(distributionSetRepository.findAll()).hasSize(1); + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, Boolean.FALSE, Boolean.TRUE) + .getTotalElements()).isEqualTo(1); + } + + @Test + @Description("Queries and loads the metadata related to a given software module.") + public void findAllDistributionSetMetadataByDsId() { + // create a DS + DistributionSet ds1 = TestDataUtil.generateDistributionSet("testDs1", softwareManagement, + distributionSetManagement); + DistributionSet ds2 = TestDataUtil.generateDistributionSet("testDs2", softwareManagement, + distributionSetManagement); + + for (int index = 0; index < 10; index++) { + + ds1 = distributionSetManagement + .createDistributionSetMetadata(new DistributionSetMetadata("key" + index, ds1, "value" + index)) + .getDistributionSet(); + } + + for (int index = 0; index < 20; index++) { + + ds2 = distributionSetManagement + .createDistributionSetMetadata(new DistributionSetMetadata("key" + index, ds2, "value" + index)) + .getDistributionSet(); + } + + final Page metadataOfDs1 = distributionSetManagement + .findDistributionSetMetadataByDistributionSetId(ds1.getId(), new PageRequest(0, 100)); + + final Page metadataOfDs2 = distributionSetManagement + .findDistributionSetMetadataByDistributionSetId(ds2.getId(), new PageRequest(0, 100)); + + assertThat(metadataOfDs1.getNumberOfElements()).isEqualTo(10); + assertThat(metadataOfDs1.getTotalElements()).isEqualTo(10); + + assertThat(metadataOfDs2.getNumberOfElements()).isEqualTo(20); + assertThat(metadataOfDs2.getTotalElements()).isEqualTo(20); + } + + @Test + @Description("Deltes a DS that is no in use. Expected behaviour is a soft delete on the database, i.e. only marked as " + + "deleted, kept eas refernce and unavailable for future use..") + public void deleteAssignedDistributionSet() { + DistributionSet ds1 = TestDataUtil.generateDistributionSet("ds-1", softwareManagement, + distributionSetManagement); + DistributionSet ds2 = TestDataUtil.generateDistributionSet("ds-2", softwareManagement, + distributionSetManagement); + DistributionSet dsAssigned = TestDataUtil.generateDistributionSet("ds-3", softwareManagement, + distributionSetManagement); + + ds1 = distributionSetManagement.findDistributionSetByNameAndVersion(ds1.getName(), ds1.getVersion()); + ds2 = distributionSetManagement.findDistributionSetByNameAndVersion(ds2.getName(), ds2.getVersion()); + + // create assigned DS + dsAssigned = distributionSetManagement.findDistributionSetByNameAndVersion(dsAssigned.getName(), + dsAssigned.getVersion()); + final Target target = new Target("4712"); + final Target savedTarget = targetManagement.createTarget(target); + final List toAssign = new ArrayList(); + toAssign.add(savedTarget); + deploymentManagement.assignDistributionSet(dsAssigned, toAssign); + + // delete a ds + assertThat(distributionSetRepository.findAll()).hasSize(3); + distributionSetManagement.deleteDistributionSet(dsAssigned.getId()); + + // not assigned so not marked as deleted + assertThat(distributionSetRepository.findAll()).hasSize(3); + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, Boolean.FALSE, Boolean.TRUE) + .getTotalElements()).isEqualTo(2); + } + + /** + * helper method which re-orders a list as expected. Re-orders the given + * distribution set in the order as given and returns a new list with the + * new order. + * + * @param dsThree + * @param buildDistributionSets + * @return + */ + private List reOrderDSList(final Iterable buildDistributionSets, + final DistributionSet... ds) { + final List reOrderedList = new ArrayList<>(); + + final Iterator iterator = buildDistributionSets.iterator(); + while (iterator.hasNext()) { + final DistributionSet next = iterator.next(); + int reorder = -1; + for (int index = 0; index < ds.length; index++) { + if (next.equals(ds[index])) { + reorder = index; + } + } + if (reorder >= 0) { + reOrderedList.add(reorder, next); + } else { + reOrderedList.add(next); + } + } + + return reOrderedList; + } + + private Target sendUpdateActionStatusToTarget(final Status status, final Action updActA, final Target t, + final String... msgs) { + updActA.setStatus(status); + + final ActionStatus statusMessages = new ActionStatus(); + statusMessages.setAction(updActA); + statusMessages.setOccurredAt(System.currentTimeMillis()); + statusMessages.setStatus(status); + for (final String msg : msgs) { + statusMessages.addMessage(msg); + } + controllerManagament.addUpdateActionStatus(statusMessages, updActA); + return targetManagement.findTargetByControllerID(t.getControllerId()); + } + +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/SoftwareManagementTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/SoftwareManagementTest.java new file mode 100755 index 000000000..dc9a654c7 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/SoftwareManagementTest.java @@ -0,0 +1,562 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.apache.commons.lang3.RandomUtils; +import org.eclipse.hawkbit.AbstractIntegrationTestWithMongoDB; +import org.eclipse.hawkbit.RandomGeneratedInputStream; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.WithUser; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Artifact; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetTag; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.junit.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +@Features("Component Tests - Repository") +@Stories("Software Management") +public class SoftwareManagementTest extends AbstractIntegrationTestWithMongoDB { + + @Test + @Description("searched for software modules based on the various filter options, e.g. name,desc,type, version.") + public void findSoftwareModuleByFilters() { + final SoftwareModule ah = softwareManagement + .createSoftwareModule(new SoftwareModule(appType, "agent-hub", "1.0.1", null, "")); + final SoftwareModule jvm = softwareManagement + .createSoftwareModule(new SoftwareModule(runtimeType, "oracle-jre", "1.7.2", null, "")); + final SoftwareModule os = softwareManagement + .createSoftwareModule(new SoftwareModule(osType, "poky", "3.0.2", null, "")); + + final SoftwareModule ah2 = softwareManagement + .createSoftwareModule(new SoftwareModule(appType, "agent-hub", "1.0.2", null, "")); + DistributionSet ds = distributionSetManagement.createDistributionSet( + TestDataUtil.buildDistributionSet("ds-1", "1.0.1", standardDsType, os, jvm, ah2)); + + final Target target = targetManagement.createTarget(new Target("test123")); + ds = assignSet(target, ds).getDistributionSet(); + + // standard searches + assertThat(softwareManagement.findSoftwareModuleByFilters(pageReq, "poky", osType).getContent()).hasSize(1); + assertThat(softwareManagement.findSoftwareModuleByFilters(pageReq, "poky", osType).getContent().get(0)) + .isEqualTo(os); + assertThat(softwareManagement.findSoftwareModuleByFilters(pageReq, "oracle%", runtimeType).getContent()) + .hasSize(1); + assertThat(softwareManagement.findSoftwareModuleByFilters(pageReq, "oracle%", runtimeType).getContent().get(0)) + .isEqualTo(jvm); + assertThat(softwareManagement.findSoftwareModuleByFilters(pageReq, "1.0.1", appType).getContent()).hasSize(1); + assertThat(softwareManagement.findSoftwareModuleByFilters(pageReq, "1.0.1", appType).getContent().get(0)) + .isEqualTo(ah); + assertThat(softwareManagement.findSoftwareModuleByFilters(pageReq, "1.0%", appType).getContent()).hasSize(2); + + // no we search with on entity marked as deleted + softwareManagement.deleteSoftwareModule( + softwareManagement.findSoftwareModuleByAssignedToAndType(pageReq, ds, appType).getContent().get(0)); + + assertThat(softwareManagement.findSoftwareModuleByFilters(pageReq, "1.0%", appType).getContent()).hasSize(1); + assertThat(softwareManagement.findSoftwareModuleByFilters(pageReq, "1.0%", appType).getContent().get(0)) + .isEqualTo(ah); + } + + private Action assignSet(final Target target, final DistributionSet ds) { + deploymentManagement.assignDistributionSet(ds.getId(), new String[] { target.getControllerId() }); + assertThat( + targetManagement.findTargetByControllerID(target.getControllerId()).getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.PENDING); + assertThat(targetManagement.findTargetByControllerID(target.getControllerId()).getAssignedDistributionSet()) + .isEqualTo(ds); + final Action action = actionRepository.findByTargetAndDistributionSet(pageReq, target, ds).getContent().get(0); + assertThat(action).isNotNull(); + return action; + } + + @Test + @Description("Searches for software modules based on a list of IDs.") + public void findSoftwareModulesByIdAndType() { + + final List modules = new ArrayList(); + + modules.add(softwareManagement.createSoftwareModule(new SoftwareModule(osType, "poky-una", "3.0.2", null, "")) + .getId()); + modules.add(softwareManagement.createSoftwareModule(new SoftwareModule(osType, "poky-u2na", "3.0.3", null, "")) + .getId()); + modules.add(624355263L); + + assertThat(softwareManagement.findSoftwareModulesById(modules)).hasSize(2); + } + + @Test + @Description("Tests the successfull deletion of software module types. Both unused (hard delete) and used ones (soft delete).") + public void deleteAssignedAndUnassignedSoftwareModuleTypes() { + assertThat(softwareManagement.findSoftwareModuleTypesAll(pageReq)).hasSize(3).contains(osType, runtimeType, + appType); + + SoftwareModuleType type = softwareManagement.createSoftwareModuleType( + new SoftwareModuleType("bundle", "OSGi Bundle", "fancy stuff", Integer.MAX_VALUE)); + + assertThat(softwareManagement.findSoftwareModuleTypesAll(pageReq)).hasSize(4).contains(osType, runtimeType, + appType, type); + + // delete unassigned + softwareManagement.deleteSoftwareModuleType(type); + assertThat(softwareManagement.findSoftwareModuleTypesAll(pageReq)).hasSize(3).contains(osType, runtimeType, + appType); + assertThat(softwareModuleTypeRepository.findAll()).hasSize(3).contains(osType, runtimeType, appType); + + type = softwareManagement.createSoftwareModuleType( + new SoftwareModuleType("bundle2", "OSGi Bundle2", "fancy stuff", Integer.MAX_VALUE)); + + assertThat(softwareManagement.findSoftwareModuleTypesAll(pageReq)).hasSize(4).contains(osType, runtimeType, + appType, type); + + softwareManagement + .createSoftwareModule(new SoftwareModule(type, "Test SM", "1.0", "cool module", "from meeee")); + + // delete assigned + softwareManagement.deleteSoftwareModuleType(type); + assertThat(softwareManagement.findSoftwareModuleTypesAll(pageReq)).hasSize(3).contains(osType, runtimeType, + appType); + + assertThat(softwareModuleTypeRepository.findAll()).hasSize(4).contains(osType, runtimeType, appType, + softwareModuleTypeRepository.findOne(type.getId())); + } + + @Test + @Description("Deletes an artifact, which is not assigned to a Distribution Set") + public void hardDeleteOfNotAssignedArtifact() { + + // [STEP1]: Create SoftwareModuleX with Artifacts + SoftwareModule unassignedModule = createSoftwareModuleWithArtifacts(osType, "moduleX", "3.0.2", 2); + Iterator artifactsIt = unassignedModule.getArtifacts().iterator(); + Artifact artifact1 = artifactsIt.next(); + Artifact artifact2 = artifactsIt.next(); + + // [STEP2]: Delete unassigned SoftwareModule + softwareManagement.deleteSoftwareModule(unassignedModule); + + // [VERIFY EXPECTED RESULT]: + // verify: SoftwareModule is deleted + assertThat(softwareModuleRepository.findAll()).hasSize(0); + assertThat(softwareManagement.findSoftwareModuleById(unassignedModule.getId())).isNull(); + + // verify: binary data of artifact is deleted + assertArtfiactNull(artifact1, artifact2); + + // verify: meta data of artifact is deleted + assertThat(artifactRepository.findOne(artifact1.getId())).isNull(); + assertThat(artifactRepository.findOne(artifact2.getId())).isNull(); + } + + @Test + @Description("Deletes an artifact, which is assigned to a Distribution Set") + public void softDeleteOfAssignedArtifact() { + + // Init DistributionSet + DistributionSet disSet = distributionSetManagement + .createDistributionSet(new DistributionSet("ds1", "v1.0", "test ds", standardDsType, null)); + + // [STEP1]: Create SoftwareModuleX with ArtifactX + SoftwareModule assignedModule = createSoftwareModuleWithArtifacts(osType, "moduleX", "3.0.2", 2); + + // [STEP2]: Assign SoftwareModule to DistributionSet + distributionSetManagement.assignSoftwareModules(disSet, Sets.newHashSet(assignedModule)); + + // [STEP3]: Delete the assigned SoftwareModule + softwareManagement.deleteSoftwareModule(assignedModule); + + // [VERIFY EXPECTED RESULT]: + // verify: assignedModule is marked as deleted + assignedModule = softwareManagement.findSoftwareModuleById(assignedModule.getId()); + assertTrue(assignedModule.isDeleted()); + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + assertThat(softwareModuleRepository.findAll()).hasSize(1); + + // verify: binary data is deleted + Iterator artifactsIt = assignedModule.getArtifacts().iterator(); + Artifact artifact1 = artifactsIt.next(); + Artifact artifact2 = artifactsIt.next(); + assertArtfiactNull(artifact1, artifact2); + + // verify: artifact meta data is still available + assertThat(artifactRepository.findOne(artifact1.getId())).isNotNull(); + assertThat(artifactRepository.findOne(artifact2.getId())).isNotNull(); + } + + @Test + @Description("Delete an artifact, which has been assigned to a rolled out DistributionSet in the past") + public void softDeleteOfHistoricalAssignedArtifact() { + + // Init target and DistributionSet + final Target target = targetManagement.createTarget(new Target("test123")); + DistributionSet disSet = distributionSetManagement + .createDistributionSet(new DistributionSet("ds1", "v1.0", "test ds", standardDsType, null)); + + // [STEP1]: Create SoftwareModuleX and include the new ArtifactX + SoftwareModule assignedModule = createSoftwareModuleWithArtifacts(osType, "moduleX", "3.0.2", 2); + + // [STEP2]: Assign SoftwareModule to DistributionSet + distributionSetManagement.assignSoftwareModules(disSet, Sets.newHashSet(assignedModule)); + + // [STEP3]: Assign DistributionSet to a Device + deploymentManagement.assignDistributionSet(disSet, Lists.newArrayList(target)); + + // [STEP4]: Delete the DistributionSet + distributionSetManagement.deleteDistributionSet(disSet); + + // [STEP5]: Delete the assigned SoftwareModule + softwareManagement.deleteSoftwareModule(assignedModule); + + // [VERIFY EXPECTED RESULT]: + // verify: assignedModule is marked as deleted + assignedModule = softwareManagement.findSoftwareModuleById(assignedModule.getId()); + assertTrue(assignedModule.isDeleted()); + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + assertThat(softwareModuleRepository.findAll()).hasSize(1); + + // verify: binary data is deleted + Iterator artifactsIt = assignedModule.getArtifacts().iterator(); + Artifact artifact1 = artifactsIt.next(); + Artifact artifact2 = artifactsIt.next(); + assertArtfiactNull(artifact1, artifact2); + + // verify: artifact meta data is still available + assertThat(artifactRepository.findOne(artifact1.getId())).isNotNull(); + assertThat(artifactRepository.findOne(artifact2.getId())).isNotNull(); + } + + @Test + @Description("Delete an softwaremodule with an artifact, which is also used by another softwaremodule.") + public void deleteSoftwareModulesWithSharedArtifact() throws IOException { + + // Precondition: Make sure MongoDB is Empty + assertThat(operations.find(new Query())).hasSize(0); + + // Init artifact binary data, target and DistributionSets + byte[] source = RandomUtils.nextBytes(1024); + + // [STEP1]: Create SoftwareModuleX and add a new ArtifactX + SoftwareModule moduleX = createSoftwareModuleWithArtifacts(osType, "modulex", "v1.0", 0); + + // [STEP2]: Create newArtifactX and add it to SoftwareModuleX + artifactManagement.createLocalArtifact(new ByteArrayInputStream(source), moduleX.getId(), "artifactx", false); + moduleX = softwareManagement.findSoftwareModuleWithDetails(moduleX.getId()); + Artifact artifactX = moduleX.getArtifacts().iterator().next(); + + // [STEP3]: Create SoftwareModuleY and add the same ArtifactX + SoftwareModule moduleY = createSoftwareModuleWithArtifacts(osType, "moduley", "v1.0", 0); + + // [STEP4]: Assign the same ArtifactX to SoftwareModuleY + artifactManagement.createLocalArtifact(new ByteArrayInputStream(source), moduleY.getId(), "artifactx", false); + moduleY = softwareManagement.findSoftwareModuleWithDetails(moduleY.getId()); + Artifact artifactY = moduleY.getArtifacts().iterator().next(); + + // verify: that only one entry was created in mongoDB + assertThat(operations.find(new Query())).hasSize(1); + + // [STEP5]: Delete SoftwareModuleX + softwareManagement.deleteSoftwareModule(moduleX); + + // [VERIFY EXPECTED RESULT]: + // verify: SoftwareModuleX is deleted, and ModuelY still exists + assertThat(softwareModuleRepository.findAll()).hasSize(1); + assertThat(softwareManagement.findSoftwareModuleById(moduleX.getId())).isNull(); + assertThat(softwareManagement.findSoftwareModuleById(moduleY.getId())).isNotNull(); + + // verify: binary data of artifact is not deleted + assertArtfiactNotNull(artifactY); + + // verify: meta data of artifactX is deleted + assertThat(artifactRepository.findOne(artifactX.getId())).isNull(); + + // verify: meta data of artifactY is not deleted + assertThat(artifactRepository.findOne(artifactY.getId())).isNotNull(); + } + + @Test + @Description("Delete two assigned softwaremodules which share an artifact.") + public void deleteMultipleSoftwareModulesWhichShareAnArtifact() throws IOException { + + // Precondition: Make sure MongoDB is Empty + assertThat(operations.find(new Query())).hasSize(0); + + // Init artifact binary data, target and DistributionSets + final byte[] source = RandomUtils.nextBytes(1024); + final Target target = targetManagement.createTarget(new Target("test123")); + final DistributionSet disSetX = distributionSetManagement + .createDistributionSet(new DistributionSet("dsX", "v1.0", "test dsX", standardDsType, null)); + final DistributionSet disSetY = distributionSetManagement + .createDistributionSet(new DistributionSet("dsY", "v1.0", "test dsY", standardDsType, null)); + + // [STEP1]: Create SoftwareModuleX and add a new ArtifactX + SoftwareModule moduleX = createSoftwareModuleWithArtifacts(osType, "modulex", "v1.0", 0); + + artifactManagement.createLocalArtifact(new ByteArrayInputStream(source), moduleX.getId(), "artifactx", false); + moduleX = softwareManagement.findSoftwareModuleWithDetails(moduleX.getId()); + Artifact artifactX = moduleX.getArtifacts().iterator().next(); + + // [STEP2]: Create SoftwareModuleY and add the same ArtifactX + SoftwareModule moduleY = createSoftwareModuleWithArtifacts(osType, "moduley", "v1.0", 0); + + artifactManagement.createLocalArtifact(new ByteArrayInputStream(source), moduleY.getId(), "artifactx", false); + moduleY = softwareManagement.findSoftwareModuleWithDetails(moduleY.getId()); + Artifact artifactY = moduleY.getArtifacts().iterator().next(); + + // verify: that only one entry was created in mongoDB + assertThat(operations.find(new Query())).hasSize(1); + + // [STEP3]: Assign SoftwareModuleX to DistributionSetX and to target + distributionSetManagement.assignSoftwareModules(disSetX, Sets.newHashSet(moduleX)); + deploymentManagement.assignDistributionSet(disSetX, Lists.newArrayList(target)); + + // [STEP4]: Assign SoftwareModuleY to DistributionSet and to target + distributionSetManagement.assignSoftwareModules(disSetY, Sets.newHashSet(moduleY)); + deploymentManagement.assignDistributionSet(disSetY, Lists.newArrayList(target)); + + // [STEP5]: Delete SoftwareModuleX + softwareManagement.deleteSoftwareModule(moduleX); + + // [STEP6]: Delete SoftwareModuleY + softwareManagement.deleteSoftwareModule(moduleY); + + // [VERIFY EXPECTED RESULT]: + moduleX = softwareManagement.findSoftwareModuleById(moduleX.getId()); + moduleY = softwareManagement.findSoftwareModuleById(moduleY.getId()); + + // verify: SoftwareModuleX and SofwtareModule are marked as deleted + assertThat(moduleX).isNotNull(); + assertThat(moduleY).isNotNull(); + assertTrue(moduleX.isDeleted()); + assertTrue(moduleY.isDeleted()); + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + assertThat(softwareModuleRepository.findAll()).hasSize(2); + + // verify: binary data of artifact is deleted + assertArtfiactNull(artifactX, artifactY); + + // verify: meta data of artifactX and artifactY is not deleted + assertThat(artifactRepository.findOne(artifactY.getId())).isNotNull(); + } + + private SoftwareModule createSoftwareModuleWithArtifacts(SoftwareModuleType type, String name, String version, + int numberArtifacts) { + + long countSoftwareModule = softwareModuleRepository.count(); + + // create SoftwareModule + SoftwareModule softwareModule = softwareManagement + .createSoftwareModule(new SoftwareModule(type, name, version, "description of artifact " + name, "")); + + for (int i = 0; i < numberArtifacts; i++) { + artifactManagement.createLocalArtifact(new RandomGeneratedInputStream(5 * 1024), softwareModule.getId(), + "file" + (i + 1), false); + } + + // Verify correct Creation of SoftwareModule and corresponding artifacts + softwareModule = softwareManagement.findSoftwareModuleWithDetails(softwareModule.getId()); + assertThat(softwareModuleRepository.findAll()).hasSize((int) countSoftwareModule + 1); + + List artifacts = softwareModule.getArtifacts(); + + assertThat(artifacts).hasSize(numberArtifacts); + if (numberArtifacts != 0) { + assertArtfiactNotNull(artifacts.toArray(new Artifact[artifacts.size()])); + } + + artifacts.forEach(artifact -> { + assertThat(artifactRepository.findOne(artifact.getId())).isNotNull(); + }); + return softwareModule; + } + + private void assertArtfiactNotNull(final Artifact... results) { + assertThat(artifactRepository.findAll()).hasSize(results.length); + for (final Artifact result : results) { + assertThat(result.getId()).isNotNull(); + assertThat(operations.findOne(new Query() + .addCriteria(Criteria.where("filename").is(((LocalArtifact) result).getGridFsFileName())))) + .isNotNull(); + } + } + + private void assertArtfiactNull(final Artifact... results) { + for (final Artifact result : results) { + assertThat(operations.findOne(new Query() + .addCriteria(Criteria.where("filename").is(((LocalArtifact) result).getGridFsFileName())))) + .isNull(); + } + } + + /** + * + * @param findAll + * @return + */ + @SuppressWarnings("rawtypes") + private Collection iterable2Collection(final Iterable iterable) { + final Collection col = new ArrayList(); + for (final Object o : iterable) { + col.add(o); + } + return col; + } + + /** + * + */ + private void printDSTags() { + System.out.println("=============================================================================="); + for (final DistributionSet d : distributionSetRepository.findAll()) { + System.out.printf("%s\t[", d.getName()); + for (final DistributionSetTag t : d.getTags()) { + System.out.printf("%s ", t.getName()); + } + System.out.println("]"); + } + } + + @Test + @Description("Checks that metadata for a software module can be created.") + public void createSoftwareModuleMetadata() { + + final String knownKey1 = "myKnownKey1"; + final String knownValue1 = "myKnownValue1"; + + final String knownKey2 = "myKnownKey2"; + final String knownValue2 = "myKnownValue2"; + + final SoftwareModule ah = softwareManagement + .createSoftwareModule(new SoftwareModule(appType, "agent-hub", "1.0.1", null, "")); + + assertThat(ah.getOptLockRevision()).isEqualTo(1L); + + final SoftwareModuleMetadata swMetadata1 = new SoftwareModuleMetadata(knownKey1, ah, knownValue1); + + final SoftwareModuleMetadata swMetadata2 = new SoftwareModuleMetadata(knownKey2, ah, knownValue2); + + final List softwareModuleMetadata = softwareManagement + .createSoftwareModuleMetadata(Lists.newArrayList(swMetadata1, swMetadata2)); + + final SoftwareModule changedLockRevisionModule = softwareManagement.findSoftwareModuleById(ah.getId()); + assertThat(changedLockRevisionModule.getOptLockRevision()).isEqualTo(2L); + + assertThat(softwareModuleMetadata).hasSize(2); + assertThat(softwareModuleMetadata.get(0)).isNotNull(); + assertThat(softwareModuleMetadata.get(0).getValue()).isEqualTo(knownValue1); + assertThat(softwareModuleMetadata.get(0).getId().getKey()).isEqualTo(knownKey1); + assertThat(softwareModuleMetadata.get(0).getSoftwareModule().getId()).isEqualTo(ah.getId()); + } + + @Test + @WithUser(allSpPermissions = true) + @Description("Checks that metadata for a software module can be updated.") + public void updateSoftwareModuleMetadata() throws InterruptedException { + final String knownKey = "myKnownKey"; + final String knownValue = "myKnownValue"; + final String knownUpdateValue = "myNewUpdatedValue"; + + // create a base software module + final SoftwareModule ah = softwareManagement + .createSoftwareModule(new SoftwareModule(appType, "agent-hub", "1.0.1", null, "")); + // initial opt lock revision must be 1 + assertThat(ah.getOptLockRevision()).isEqualTo(1L); + + // create an software module meta data entry + final List softwareModuleMetadata = softwareManagement.createSoftwareModuleMetadata( + Collections.singleton(new SoftwareModuleMetadata(knownKey, ah, knownValue))); + assertThat(softwareModuleMetadata).hasSize(1); + // base software module should have now the opt lock revision one + // because we are modifying the + // base software module + SoftwareModule changedLockRevisionModule = softwareManagement.findSoftwareModuleById(ah.getId()); + assertThat(changedLockRevisionModule.getOptLockRevision()).isEqualTo(2L); + + // modifying the meta data value + softwareModuleMetadata.get(0).setValue(knownUpdateValue); + softwareModuleMetadata.get(0).setSoftwareModule(softwareManagement.findSoftwareModuleById(ah.getId())); + softwareModuleMetadata.get(0).setKey(knownKey); + + // update the software module metadata + Thread.sleep(100); + final SoftwareModuleMetadata updated = softwareManagement + .updateSoftwareModuleMetadata(softwareModuleMetadata.get(0)); + // we are updating the sw meta data so also modiying the base software + // module so opt lock + // revision must be two + changedLockRevisionModule = softwareManagement.findSoftwareModuleById(ah.getId()); + assertThat(changedLockRevisionModule.getOptLockRevision()).isEqualTo(3L); + + // verify updated meta data contains the updated value + assertThat(updated).isNotNull(); + assertThat(updated.getValue()).isEqualTo(knownUpdateValue); + assertThat(updated.getId().getKey()).isEqualTo(knownKey); + assertThat(updated.getSoftwareModule().getId()).isEqualTo(ah.getId()); + } + + @Test + @Description("Queries and loads the metadata related to a given software module.") + public void findAllSoftwareModuleMetadataBySwId() { + + SoftwareModule sw1 = softwareManagement + .createSoftwareModule(new SoftwareModule(appType, "agent-hub", "1.0.1", null, "")); + + SoftwareModule sw2 = softwareManagement + .createSoftwareModule(new SoftwareModule(osType, "os", "1.0.1", null, "")); + + for (int index = 0; index < 10; index++) { + sw1 = softwareManagement + .createSoftwareModuleMetadata(new SoftwareModuleMetadata("key" + index, sw1, "value" + index)) + .getSoftwareModule(); + } + + for (int index = 0; index < 20; index++) { + sw2 = softwareManagement + .createSoftwareModuleMetadata(new SoftwareModuleMetadata("key" + index, sw2, "value" + index)) + .getSoftwareModule(); + } + + final Page metadataOfSw1 = softwareManagement + .findSoftwareModuleMetadataBySoftwareModuleId(sw1.getId(), new PageRequest(0, 100)); + + final Page metadataOfSw2 = softwareManagement + .findSoftwareModuleMetadataBySoftwareModuleId(sw2.getId(), new PageRequest(0, 100)); + + assertThat(metadataOfSw1.getNumberOfElements()).isEqualTo(10); + assertThat(metadataOfSw1.getTotalElements()).isEqualTo(10); + + assertThat(metadataOfSw2.getNumberOfElements()).isEqualTo(20); + assertThat(metadataOfSw2.getTotalElements()).isEqualTo(20); + } +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/SystemManagementTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/SystemManagementTest.java new file mode 100755 index 000000000..5d5368074 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/SystemManagementTest.java @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import static org.fest.assertions.api.Assertions.assertThat; + +import java.util.concurrent.Callable; + +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.WithSpringAuthorityRule; +import org.eclipse.hawkbit.repository.model.TenantConfiguration; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationKey; +import org.junit.Test; +import org.springframework.core.convert.ConversionFailedException; + +import ru.yandex.qatools.allure.annotations.Description; + +/** + * + * + * + */ +public class SystemManagementTest extends AbstractIntegrationTest { + + @Test + @Description("Ensures that findTenants returns all tenants and not only restricted to the tenant which currently is logged in") + public void findTenantsReturnsAllTenantsNotOnlyWhichLoggedIn() throws Exception { + final String knownTenant1 = "knownTenant1"; + final String knownTenant2 = "knownTenant2"; + securityRule.runAs(WithSpringAuthorityRule.withUserAndTenant("bumlux", knownTenant1), new Callable() { + @Override + public Void call() throws Exception { + systemManagement.getTenantMetadata(knownTenant1); + return null; + } + }); + + securityRule.runAs(WithSpringAuthorityRule.withUserAndTenant("bumlux", knownTenant2), new Callable() { + @Override + public Void call() throws Exception { + systemManagement.getTenantMetadata(knownTenant2); + return null; + } + }); + } + + @Test + @Description("Tests that tenant specific configuration can be persisted and in case the tenant does not have specific configuration the default from environment is used instead.") + public void storeTenantSpecificConfiguration() { + final TenantConfigurationKey configKey = TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED; + final String envPropertyDefault = environment.getProperty(configKey.getDefaultKeyName()); + assertThat(envPropertyDefault).isNotNull(); + + // get the configuration from the system management + final String defaultConfigValue = systemManagement.getConfigurationValue(configKey, String.class); + assertThat(envPropertyDefault).isEqualTo(defaultConfigValue); + + // update the tenant specific configuration + final String newConfigurationValue = "thisIsAnotherValueForPolling"; + assertThat(newConfigurationValue).isNotEqualTo(defaultConfigValue); + systemManagement + .addOrUpdateConfiguration(new TenantConfiguration(configKey.getKeyName(), newConfigurationValue)); + + // verify that new configuration value is used + final String updatedConfigurationValue = systemManagement.getConfigurationValue(configKey, String.class); + assertThat(updatedConfigurationValue).isEqualTo(newConfigurationValue); + assertThat(systemManagement.getTenantConfigurations()).hasSize(1); + } + + @Test + @Description("Tests that the tenant specific configuration can be updated") + public void updateTenantSpecifcConfiguration() { + final TenantConfigurationKey configKey = TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED; + final String value1 = "firstValue"; + final String value2 = "secondValue"; + + // add value first + systemManagement.addOrUpdateConfiguration(new TenantConfiguration(configKey.getKeyName(), value1)); + assertThat(systemManagement.getConfigurationValue(configKey, String.class)).isEqualTo(value1); + + // update to value second + systemManagement.addOrUpdateConfiguration(new TenantConfiguration(configKey.getKeyName(), value2)); + assertThat(systemManagement.getConfigurationValue(configKey, String.class)).isEqualTo(value2); + } + + @Test + @Description("Tests that the configuration value can be converted from String to Integer automatically") + public void tenantConfigurationValueConversion() { + final TenantConfigurationKey configKey = TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED; + final Integer value1 = 123; + systemManagement + .addOrUpdateConfiguration(new TenantConfiguration(configKey.getKeyName(), String.valueOf(value1))); + assertThat(systemManagement.getConfigurationValue(configKey, Integer.class)).isEqualTo(value1); + } + + @Test(expected = ConversionFailedException.class) + @Description("Tests that the get configuration throws exception in case the value cannot be automatically converted from String to Integer") + public void wrongTenantConfigurationValueConversionThrowsException() { + final TenantConfigurationKey configKey = TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED; + final String value1 = "thisIsNotANumber"; + // add value as String + systemManagement + .addOrUpdateConfiguration(new TenantConfiguration(configKey.getKeyName(), String.valueOf(value1))); + // try to get it as Integer + systemManagement.getConfigurationValue(configKey, Integer.class); + } + + @Test + @Description("Tests that a deletion of a tenant specific configuration deletes it from the database.") + public void deleteConfigurationReturnNullConfiguration() { + final TenantConfigurationKey configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY; + + // gateway token does not have default value so no configuration value + // is should be available + final String defaultConfigValue = systemManagement.getConfigurationValue(configKey, String.class); + assertThat(defaultConfigValue).isNull(); + + // update the tenant specific configuration + final String newConfigurationValue = "thisIsAnotherValueForPolling"; + assertThat(newConfigurationValue).isNotEqualTo(defaultConfigValue); + systemManagement + .addOrUpdateConfiguration(new TenantConfiguration(configKey.getKeyName(), newConfigurationValue)); + + // verify that new configuration value is used + final String updatedConfigurationValue = systemManagement.getConfigurationValue(configKey, String.class); + assertThat(updatedConfigurationValue).isEqualTo(newConfigurationValue); + + // delete the tenant specific configuration + systemManagement.deleteConfiguration(configKey); + // ensure that now gateway token is set again, because is deleted and + // must be null now + assertThat(systemManagement.getConfigurationValue(configKey, String.class)).isNull(); + } +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/TagManagementTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/TagManagementTest.java new file mode 100755 index 000000000..6f938056d --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/TagManagementTest.java @@ -0,0 +1,372 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.repository.DistributionSetFilter.DistributionSetFilterBuilder; +import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetTag; +import org.eclipse.hawkbit.repository.model.Tag; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetTag; +import org.junit.Test; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.Lists; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * Test class for {@link TagManagement}. + * + * + * + */ +@Features("Component Tests - Repository") +@Stories("Tag Management") +// TODO: fully document tests -> @Description for long text and reasonable +// method name as short text +public class TagManagementTest extends AbstractIntegrationTest { + public TagManagementTest() { + LOG = LoggerFactory.getLogger(TagManagementTest.class); + } + + @Test + @Description("Full DS tag lifecycle tested. Create tags, assign them to sets and delete the tags.") + public void createAndAssignAndDeleteDistributionSetTags() { + final List dsAs = TestDataUtil.generateDistributionSets("DS-A", 20, softwareManagement, + distributionSetManagement); + final List dsBs = TestDataUtil.generateDistributionSets("DS-B", 10, softwareManagement, + distributionSetManagement); + final List dsCs = TestDataUtil.generateDistributionSets("DS-C", 25, softwareManagement, + distributionSetManagement); + final List dsABs = TestDataUtil.generateDistributionSets("DS-AB", 5, softwareManagement, + distributionSetManagement); + final List dsACs = TestDataUtil.generateDistributionSets("DS-AC", 11, softwareManagement, + distributionSetManagement); + final List dsBCs = TestDataUtil.generateDistributionSets("DS-BC", 13, softwareManagement, + distributionSetManagement); + final List dsABCs = TestDataUtil.generateDistributionSets("DS-ABC", 9, softwareManagement, + distributionSetManagement); + + final DistributionSetTag tagA = tagManagement.createDistributionSetTag(new DistributionSetTag("A")); + final DistributionSetTag tagB = tagManagement.createDistributionSetTag(new DistributionSetTag("B")); + final DistributionSetTag tagC = tagManagement.createDistributionSetTag(new DistributionSetTag("C")); + final DistributionSetTag tagX = tagManagement.createDistributionSetTag(new DistributionSetTag("X")); + final DistributionSetTag tagY = tagManagement.createDistributionSetTag(new DistributionSetTag("Y")); + + distributionSetManagement.toggleTagAssignment(dsAs, tagA); + distributionSetManagement.toggleTagAssignment(dsBs, tagB); + distributionSetManagement.toggleTagAssignment(dsCs, tagC); + + distributionSetManagement.toggleTagAssignment(dsABs, tagManagement.findDistributionSetTag(tagA.getName())); + distributionSetManagement.toggleTagAssignment(dsABs, tagManagement.findDistributionSetTag(tagB.getName())); + + distributionSetManagement.toggleTagAssignment(dsACs, tagManagement.findDistributionSetTag(tagA.getName())); + distributionSetManagement.toggleTagAssignment(dsACs, tagManagement.findDistributionSetTag(tagC.getName())); + + distributionSetManagement.toggleTagAssignment(dsBCs, tagManagement.findDistributionSetTag(tagB.getName())); + distributionSetManagement.toggleTagAssignment(dsBCs, tagManagement.findDistributionSetTag(tagC.getName())); + + distributionSetManagement.toggleTagAssignment(dsABCs, tagManagement.findDistributionSetTag(tagA.getName())); + distributionSetManagement.toggleTagAssignment(dsABCs, tagManagement.findDistributionSetTag(tagB.getName())); + distributionSetManagement.toggleTagAssignment(dsABCs, tagManagement.findDistributionSetTag(tagC.getName())); + + DistributionSetFilterBuilder distributionSetFilterBuilder; + + // search for not deleted + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsComplete(true) + .setTagNames(Lists.newArrayList(tagA.getName())); + assertEquals( + dsAs.spliterator().getExactSizeIfKnown() + dsABs.spliterator().getExactSizeIfKnown() + + dsACs.spliterator().getExactSizeIfKnown() + dsABCs.spliterator().getExactSizeIfKnown(), + distributionSetManagement.findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()) + .getTotalElements()); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsComplete(true) + .setTagNames(Lists.newArrayList(tagB.getName())); + assertEquals( + dsBs.spliterator().getExactSizeIfKnown() + dsABs.spliterator().getExactSizeIfKnown() + + dsBCs.spliterator().getExactSizeIfKnown() + dsABCs.spliterator().getExactSizeIfKnown(), + distributionSetManagement.findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()) + .getTotalElements()); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsComplete(true) + .setTagNames(Lists.newArrayList(tagC.getName())); + assertEquals( + dsCs.spliterator().getExactSizeIfKnown() + dsACs.spliterator().getExactSizeIfKnown() + + dsBCs.spliterator().getExactSizeIfKnown() + dsABCs.spliterator().getExactSizeIfKnown(), + distributionSetManagement.findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()) + .getTotalElements()); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsComplete(true) + .setTagNames(Lists.newArrayList(tagX.getName())); + assertEquals(0, distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getTotalElements()); + + assertEquals(5, distributionSetTagRepository.findAll().spliterator().getExactSizeIfKnown()); + + tagManagement.deleteDistributionSetTag(tagY.getName()); + assertEquals(4, distributionSetTagRepository.findAll().spliterator().getExactSizeIfKnown()); + tagManagement.deleteDistributionSetTag(tagX.getName()); + assertEquals(3, distributionSetTagRepository.findAll().spliterator().getExactSizeIfKnown()); + + tagManagement.deleteDistributionSetTag(tagB.getName()); + assertEquals(2, distributionSetTagRepository.findAll().spliterator().getExactSizeIfKnown()); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsComplete(Boolean.TRUE) + .setTagNames(Lists.newArrayList(tagA.getName())); + assertEquals( + dsAs.spliterator().getExactSizeIfKnown() + dsABs.spliterator().getExactSizeIfKnown() + + dsACs.spliterator().getExactSizeIfKnown() + dsABCs.spliterator().getExactSizeIfKnown(), + distributionSetManagement.findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()) + .getTotalElements()); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsComplete(Boolean.TRUE) + .setTagNames(Lists.newArrayList(tagB.getName())); + assertEquals(0, distributionSetManagement + .findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()).getTotalElements()); + + distributionSetFilterBuilder = getDistributionSetFilterBuilder().setIsComplete(Boolean.TRUE) + .setTagNames(Lists.newArrayList(tagC.getName())); + assertEquals( + dsCs.spliterator().getExactSizeIfKnown() + dsACs.spliterator().getExactSizeIfKnown() + + dsBCs.spliterator().getExactSizeIfKnown() + dsABCs.spliterator().getExactSizeIfKnown(), + distributionSetManagement.findDistributionSetsByFilters(pageReq, distributionSetFilterBuilder.build()) + .getTotalElements()); + } + + private DistributionSetFilterBuilder getDistributionSetFilterBuilder() { + return new DistributionSetFilterBuilder(); + } + + /** + * Test method for + * {@link org.eclipse.hawkbit.repository.TagManagement#findTargetTag(java.lang.String)} + * . + */ + @Test + public void testFindTargetTag() { + assertThat(targetTagRepository.findAll()).isEmpty(); + + final List tags = createTargetsWithTags(); + + assertThat(targetTagRepository.findAll()).isEqualTo(tagManagement.findAllTargetTags()).isEqualTo(tags) + .hasSize(20); + } + + /** + * Test method for + * {@link org.eclipse.hawkbit.repository.TagManagement#createTargetTag(org.eclipse.hawkbit.repository.model.TargetTag)} + * . + */ + @Test + public void testCreateTargetTag() { + assertThat(targetTagRepository.findAll()).isEmpty(); + + final Tag tag = tagManagement.createTargetTag(new TargetTag("kai1", "kai2", "colour")); + + assertThat(targetTagRepository.findByNameEquals("kai1").getDescription()).isEqualTo("kai2"); + assertThat(tagManagement.findTargetTag("kai1").getColour()).isEqualTo("colour"); + assertThat(tagManagement.findTargetTagById(tag.getId()).getColour()).isEqualTo("colour"); + } + + /** + * Test method for + * {@link org.eclipse.hawkbit.repository.TagManagement#deleteTargetTag(java.lang.String[])} + * . + */ + @Test + public void testDeleteTargetTagsStringArray() { + assertThat(targetTagRepository.findAll()).isEmpty(); + + // create test data + final Iterable tags = createTargetsWithTags(); + final TargetTag toDelete = tags.iterator().next(); + + for (final Target target : targetRepository.findAll()) { + assertThat(targetManagement.findTargetByControllerID(target.getControllerId()).getTags()) + .contains(toDelete); + } + + // delete + tagManagement.deleteTargetTag(toDelete.getName()); + + // check + for (final Target target : targetRepository.findAll()) { + assertThat(targetManagement.findTargetByControllerID(target.getControllerId()).getTags()) + .doesNotContain(toDelete); + } + assertThat(targetTagRepository.findOne(toDelete.getId())).isNull(); + assertThat(tagManagement.findAllTargetTags()).hasSize(19); + } + + @Test + @Description("Tests the creation of a target tag.") + public void updateTargetTag() { + assertThat(targetTagRepository.findAll()).isEmpty(); + + // create test data + final List tags = createTargetsWithTags(); + + // change data + final TargetTag savedAssigned = tags.iterator().next(); + savedAssigned.setName("test123"); + + // persist + tagManagement.updateTargetTag(savedAssigned); + + // check data + assertThat(tagManagement.findAllTargetTags()).hasSize(tags.size()); + assertThat(targetTagRepository.findOne(savedAssigned.getId()).getName()).isEqualTo("test123"); + assertThat(targetTagRepository.findOne(savedAssigned.getId()).getOptLockRevision()).isEqualTo(2); + } + + /** + * Test method for + * {@link org.eclipse.hawkbit.repository.TagManagement#createDistributionSetTag(org.eclipse.hawkbit.repository.model.DistributionSetTag)} + * . + */ + @Test + public void testCreateDistributionSetTag() { + assertThat(distributionSetTagRepository.findAll()).isEmpty(); + + final Tag tag = tagManagement.createDistributionSetTag(new DistributionSetTag("kai1", "kai2", "colour")); + + assertThat(distributionSetTagRepository.findByNameEquals("kai1").getDescription()).isEqualTo("kai2"); + assertThat(tagManagement.findDistributionSetTag("kai1").getColour()).isEqualTo("colour"); + assertThat(tagManagement.findDistributionSetTagById(tag.getId()).getColour()).isEqualTo("colour"); + } + + /** + * Test method for + * {@link org.eclipse.hawkbit.repository.TagManagement#createDistributionSetTags(java.lang.Iterable)} + * . + */ + @Test + public void testCreateDistributionSetTags() { + assertThat(distributionSetTagRepository.findAll()).isEmpty(); + + final List tags = createDsSetsWithTags(); + + assertThat(distributionSetTagRepository.findAll()).hasSize(tags.size()); + } + + /** + * Test method for + * {@link org.eclipse.hawkbit.repository.TagManagement#deleteDistributionSetTag(java.lang.String[])} + * . + */ + @Test + public void testDeleteDistributionSetTag() { + assertThat(distributionSetTagRepository.findAll()).isEmpty(); + + // create test data + final Iterable tags = createDsSetsWithTags(); + final DistributionSetTag toDelete = tags.iterator().next(); + + for (final DistributionSet set : distributionSetRepository.findAll()) { + assertThat(distributionSetManagement.findDistributionSetByIdWithDetails(set.getId()).getTags()) + .contains(toDelete); + } + + // delete + tagManagement.deleteDistributionSetTag(tags.iterator().next().getName()); + + // check + assertThat(distributionSetTagRepository.findOne(toDelete.getId())).isNull(); + assertThat(tagManagement.findDistributionSetTagsAll()).hasSize(19); + for (final DistributionSet set : distributionSetRepository.findAll()) { + assertThat(distributionSetManagement.findDistributionSetByIdWithDetails(set.getId()).getTags()) + .doesNotContain(toDelete); + } + } + + @Test(expected = EntityAlreadyExistsException.class) + public void testFailedDuplicateTargetTagNameException() { + tagManagement.createTargetTag(new TargetTag("A")); + tagManagement.createTargetTag(new TargetTag("A")); + } + + @Test(expected = EntityAlreadyExistsException.class) + public void testFailedDuplicateDsTagNameException() { + tagManagement.createDistributionSetTag(new DistributionSetTag("A")); + tagManagement.createDistributionSetTag(new DistributionSetTag("A")); + } + + /** + * Test method for + * {@link org.eclipse.hawkbit.repository.TagManagement#updateDistributionSetTag(org.eclipse.hawkbit.repository.model.DistributionSetTag)} + * . + */ + @Test + public void testUpdateDistributionSetTag() { + assertThat(distributionSetTagRepository.findAll()).isEmpty(); + + // create test data + final List tags = createDsSetsWithTags(); + + // change data + final DistributionSetTag savedAssigned = tags.iterator().next(); + savedAssigned.setName("test123"); + + // persist + tagManagement.updateDistributionSetTag(savedAssigned); + + // check data + assertThat(tagManagement.findDistributionSetTagsAll()).hasSize(tags.size()); + assertThat(distributionSetTagRepository.findOne(savedAssigned.getId()).getName()).isEqualTo("test123"); + } + + /** + * Test method for + * {@link org.eclipse.hawkbit.repository.TagManagement#findDistributionSetTagsAll()} + * . + */ + @Test + public void testFindDistributionSetTagsAll() { + assertThat(distributionSetTagRepository.findAll()).isEmpty(); + + final List tags = createDsSetsWithTags(); + + // test + assertThat(tagManagement.findDistributionSetTagsAll()).hasSize(tags.size()); + assertThat(distributionSetTagRepository.findAll()).hasSize(20); + } + + private List createTargetsWithTags() { + targetManagement.createTargets(TestDataUtil.generateTargets(20)); + final Iterable tags = tagManagement.createTargetTags(TestDataUtil.generateTargetTags(20)); + + tags.forEach(tag -> targetManagement.toggleTagAssignment(targetRepository.findAll(), tag)); + + return tagManagement.findAllTargetTags(); + } + + private List createDsSetsWithTags() { + + final List sets = TestDataUtil.generateDistributionSets(20, softwareManagement, + distributionSetManagement); + final Iterable tags = tagManagement + .createDistributionSetTags(TestDataUtil.generateDistributionSetTags(20)); + + tags.forEach(tag -> distributionSetManagement.toggleTagAssignment(sets, tag)); + + return tagManagement.findDistributionSetTagsAll(); + } +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagenmentTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagenmentTest.java new file mode 100755 index 000000000..7479bdac7 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagenmentTest.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import static org.junit.Assert.assertEquals; + +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.model.TargetFilterQuery; +import org.junit.Test; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * Test class for {@link TargetFilterQueryManagement}. + * + * + * + */ + +@Features("Component Tests - Repository") +@Stories("Target Filter Query Management") +public class TargetFilterQueryManagenmentTest extends AbstractIntegrationTest { + + @Test + @Description("Test creation of target filter query.") + public void createTargetFilterQuery() { + final String filterName = "new target filter"; + final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement + .createTargetFilterQuery(new TargetFilterQuery(filterName, "name==PendingTargets001")); + assertEquals("Retrieved newly created custom target filter", targetFilterQuery, + targetFilterQueryManagement.findTargetFilterQueryByName(filterName)); + } + + @Test(expected = EntityAlreadyExistsException.class) + @Description("Checks if the EntityAlreadyExistsException is thrown if a targetfilterquery with the same name are created more than once.") + public void createDuplicateTargetFilterQuery() { + final String filterName = "new target filter duplicate"; + targetFilterQueryManagement + .createTargetFilterQuery(new TargetFilterQuery(filterName, "name==PendingTargets001")); + + targetFilterQueryManagement + .createTargetFilterQuery(new TargetFilterQuery(filterName, "name==PendingTargets001")); + } + + @Test + @Description("Test deletion of target filter query.") + public void deleteTargetFilterQuery() { + final String filterName = "delete_target_filter_query"; + final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement + .createTargetFilterQuery(new TargetFilterQuery(filterName, "name==PendingTargets001")); + targetFilterQueryManagement.deleteTargetFilterQuery(targetFilterQuery.getId()); + assertEquals("Returns null as the target filter is deleted", null, + targetFilterQueryManagement.findTargetFilterQueryById(targetFilterQuery.getId())); + + } + + @Test + @Description("Test updation of target filter query.") + public void updateTargetFilterQuery() { + final String filterName = "target_filter_01"; + final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement + .createTargetFilterQuery(new TargetFilterQuery(filterName, "name==PendingTargets001")); + + final String newQuery = "status==UNKNOWN"; + targetFilterQuery.setQuery(newQuery); + targetFilterQueryManagement.updateTargetFilterQuery(targetFilterQuery); + assertEquals("Returns updated target filter query", newQuery, targetFilterQueryManagement + .findTargetFilterQueryByName(filterName).getQuery()); + + } + +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/TargetManagementSearchTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/TargetManagementSearchTest.java new file mode 100755 index 000000000..bd9884e89 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/TargetManagementSearchTest.java @@ -0,0 +1,252 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import static org.fest.assertions.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.BaseEntity; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetTag; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.junit.Test; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +import com.google.common.primitives.Ints; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +@Features("Component Tests - Repository") +@Stories("Target Management Searches") +public class TargetManagementSearchTest extends AbstractIntegrationTest { + + @Test + @Description("Tests different parameter combinations for target search operations. That includes both the test itself as a count operation with the same filters.") + public void targetSearchWithVariousFilterCombinations() { + final TargetTag targTagA = tagManagement.createTargetTag(new TargetTag("TargTag-A")); + final TargetTag targTagB = tagManagement.createTargetTag(new TargetTag("TargTag-B")); + final TargetTag targTagC = tagManagement.createTargetTag(new TargetTag("TargTag-C")); + final TargetTag targTagD = tagManagement.createTargetTag(new TargetTag("TargTag-D")); + + // TODO kzimmerm: test also installedDS (not only assignedDS) + + final DistributionSet setA = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + final String targetDsAIdPref = "targ-A"; + List targAs = targetManagement.createTargets( + TestDataUtil.buildTargetFixtures(100, targetDsAIdPref, targetDsAIdPref.concat(" description"))); + targAs = targetManagement.toggleTagAssignment(targAs, targTagA).getAssignedTargets(); + + final String targetDsBIdPref = "targ-B"; + List targBs = targetManagement.createTargets( + TestDataUtil.buildTargetFixtures(100, targetDsBIdPref, targetDsBIdPref.concat(" description"))); + targBs = targetManagement.toggleTagAssignment(targBs, targTagB).getAssignedTargets(); + targBs = targetManagement.toggleTagAssignment(targBs, targTagD).getAssignedTargets(); + + final String targetDsCIdPref = "targ-C"; + List targCs = targetManagement.createTargets( + TestDataUtil.buildTargetFixtures(100, targetDsCIdPref, targetDsCIdPref.concat(" description"))); + targCs = targetManagement.toggleTagAssignment(targCs, targTagC).getAssignedTargets(); + targCs = targetManagement.toggleTagAssignment(targCs, targTagD).getAssignedTargets(); + + final String targetDsDIdPref = "targ-D"; + final Iterable targDs = targetManagement.createTargets( + TestDataUtil.buildTargetFixtures(100, targetDsDIdPref, targetDsDIdPref.concat(" description"))); + + deploymentManagement.assignDistributionSet(setA.getId(), targCs.iterator().next().getControllerId()); + deploymentManagement.assignDistributionSet(setA.getId(), targAs.iterator().next().getControllerId()); + deploymentManagement.assignDistributionSet(setA.getId(), targBs.iterator().next().getControllerId()); + + final List unknown = new ArrayList(); + unknown.add(TargetUpdateStatus.UNKNOWN); + + final List pending = new ArrayList(); + pending.add(TargetUpdateStatus.PENDING); + + final List both = new ArrayList(); + both.add(TargetUpdateStatus.UNKNOWN); + both.add(TargetUpdateStatus.PENDING); + + final PageRequest pageReq = new PageRequest(0, 500); + // try to find several targets with different filter settings + + // TODO kzimmerm: comment and check also the content itself, not only + // the numbers + // (containsOnly) + assertThat(targetManagement.countTargetsAll()).isEqualTo(400); + + assertThat(targetManagement.findTargetByFilters(pageReq, null, null, null, Boolean.FALSE, targTagD.getName()) + .getNumberOfElements()).isEqualTo(200).isEqualTo(Ints.saturatedCast( + targetManagement.countTargetByFilters(null, null, null, Boolean.FALSE, targTagD.getName()))); + + Slice x = targetManagement.findTargetByFilters(pageReq, null, "%targ-B%", null, Boolean.FALSE, + targTagB.getName(), targTagD.getName()); + assertThat(x.getNumberOfElements()).isEqualTo(100).isEqualTo(Ints.saturatedCast(targetManagement + .countTargetByFilters(null, "%targ-B%", null, Boolean.FALSE, targTagB.getName(), targTagD.getName()))); + + x = targetManagement.findTargetByFilters(pageReq, null, "%targ-C%", setA.getId(), Boolean.FALSE, + targTagD.getName()); + assertThat(x.getNumberOfElements()).isEqualTo(1).isEqualTo(Ints.saturatedCast(targetManagement + .countTargetByFilters(null, "%targ-C%", setA.getId(), Boolean.FALSE, targTagD.getName()))); + + x = targetManagement.findTargetByFilters(pageReq, null, "%targ-A%", setA.getId(), Boolean.FALSE, + targTagD.getName()); + assertThat(x.getNumberOfElements()).isEqualTo(0).isEqualTo(Ints.saturatedCast(targetManagement + .countTargetByFilters(null, "%targ-A%", setA.getId(), Boolean.FALSE, targTagD.getName()))); + + x = targetManagement.findTargetByFilters(pageReq, null, "%targ-C%", setA.getId(), Boolean.FALSE, + targTagA.getName()); + assertThat(x.getNumberOfElements()).isEqualTo(0).isEqualTo(Ints.saturatedCast(targetManagement + .countTargetByFilters(null, "%targ-C%", setA.getId(), Boolean.FALSE, targTagA.getName()))); + + x = targetManagement.findTargetByFilters(pageReq, null, null, setA.getId(), Boolean.FALSE, null); + assertThat(x.getNumberOfElements()).isEqualTo(3).isEqualTo(Ints + .saturatedCast(targetManagement.countTargetByFilters(null, null, setA.getId(), Boolean.FALSE, null))); + + x = targetManagement.findTargetByFilters(pageReq, null, "%targ-A%", setA.getId(), Boolean.FALSE, null); + assertThat(x.getNumberOfElements()).isEqualTo(1).isEqualTo(Ints.saturatedCast( + targetManagement.countTargetByFilters(null, "%targ-A%", setA.getId(), Boolean.FALSE, null))); + + x = targetManagement.findTargetByFilters(pageReq, unknown, null, null, Boolean.FALSE, null); + assertThat(x.getNumberOfElements()).isEqualTo(397).isEqualTo( + Ints.saturatedCast(targetManagement.countTargetByFilters(unknown, null, null, Boolean.FALSE, null))); + + x = targetManagement.findTargetByFilters(pageReq, unknown, null, null, Boolean.FALSE, targTagB.getName(), + targTagD.getName()); + assertThat(x.getNumberOfElements()).isEqualTo(198).isEqualTo(Ints.saturatedCast(targetManagement + .countTargetByFilters(unknown, null, null, Boolean.FALSE, targTagB.getName(), targTagD.getName()))); + + x = targetManagement.findTargetByFilters(pageReq, unknown, null, setA.getId(), Boolean.FALSE, null); + assertThat(x.getNumberOfElements()).isEqualTo(0).isEqualTo(Ints.saturatedCast( + targetManagement.countTargetByFilters(unknown, null, setA.getId(), Boolean.FALSE, null))); + + x = targetManagement.findTargetByFilters(pageReq, unknown, "%targ-A%", null, Boolean.FALSE, null); + assertThat(x.getNumberOfElements()).isEqualTo(99).isEqualTo(Ints + .saturatedCast(targetManagement.countTargetByFilters(unknown, "%targ-A%", null, Boolean.FALSE, null))); + + x = targetManagement.findTargetByFilters(pageReq, unknown, "%targ-B%", null, Boolean.FALSE, targTagD.getName()); + assertThat(x.getNumberOfElements()).isEqualTo(99).isEqualTo(Ints.saturatedCast( + targetManagement.countTargetByFilters(unknown, "%targ-B%", null, Boolean.FALSE, targTagD.getName()))); + + x = targetManagement.findTargetByFilters(pageReq, unknown, null, null, Boolean.FALSE, targTagD.getName()); + assertThat(x.getNumberOfElements()).isEqualTo(198).isEqualTo(Ints.saturatedCast( + targetManagement.countTargetByFilters(unknown, null, null, Boolean.FALSE, targTagD.getName()))); + + x = targetManagement.findTargetByFilters(pageReq, pending, null, null, Boolean.FALSE, null); + assertThat(x.getNumberOfElements()).isEqualTo(3).isEqualTo( + Ints.saturatedCast(targetManagement.countTargetByFilters(pending, null, null, Boolean.FALSE, null))); + + x = targetManagement.findTargetByFilters(pageReq, pending, null, setA.getId(), Boolean.FALSE, null); + assertThat(x.getNumberOfElements()).isEqualTo(3).isEqualTo(Ints.saturatedCast( + targetManagement.countTargetByFilters(pending, null, setA.getId(), Boolean.FALSE, null))); + + x = targetManagement.findTargetByFilters(pageReq, pending, "%targ-A%", setA.getId(), Boolean.FALSE, null); + assertThat(x.getNumberOfElements()).isEqualTo(1).isEqualTo(Ints.saturatedCast( + targetManagement.countTargetByFilters(pending, "%targ-A%", setA.getId(), Boolean.FALSE, null))); + + x = targetManagement.findTargetByFilters(pageReq, pending, "%targ-B%", setA.getId(), Boolean.FALSE, + targTagD.getName()); + assertThat(x.getNumberOfElements()).isEqualTo(1).isEqualTo(Ints.saturatedCast(targetManagement + .countTargetByFilters(pending, "%targ-B%", setA.getId(), Boolean.FALSE, targTagD.getName()))); + + x = targetManagement.findTargetByFilters(pageReq, pending, null, setA.getId(), Boolean.FALSE, + targTagD.getName()); + assertThat(x.getNumberOfElements()).isEqualTo(2).isEqualTo(Ints.saturatedCast( + targetManagement.countTargetByFilters(pending, null, setA.getId(), Boolean.FALSE, targTagD.getName()))); + + x = targetManagement.findTargetByFilters(pageReq, pending, null, null, Boolean.FALSE, targTagD.getName()); + assertThat(x.getNumberOfElements()).isEqualTo(2).isEqualTo(Ints.saturatedCast( + targetManagement.countTargetByFilters(pending, null, null, Boolean.FALSE, targTagD.getName()))); + + // Both status: 2 pending and 198 unknown + assertThat(targetManagement.findTargetByFilters(pageReq, both, null, null, Boolean.FALSE, targTagD.getName()) + .getNumberOfElements()).isEqualTo(200).isEqualTo(Ints.saturatedCast( + targetManagement.countTargetByFilters(both, null, null, Boolean.FALSE, targTagD.getName()))); + + } + + // TODO kzimmerm: add filter tests + @Test + @Description("Tests the correct order of targets based on selected distribution set. The system expects to have an order based on installed, assigned DS.") + public void targetSearchWithVariousFilterCombinationsAndOrderByDistributionSet() { + + final List notAssigned = targetManagement + .createTargets(TestDataUtil.buildTargetFixtures(3, "not", "first description")); + List targAssigned = targetManagement + .createTargets(TestDataUtil.buildTargetFixtures(3, "assigned", "first description")); + List targInstalled = targetManagement + .createTargets(TestDataUtil.buildTargetFixtures(3, "installed", "first description")); + + final DistributionSet ds = TestDataUtil.generateDistributionSet("a", softwareManagement, + distributionSetManagement); + + targAssigned = deploymentManagement.assignDistributionSet(ds, targAssigned).getAssignedTargets(); + targInstalled = deploymentManagement.assignDistributionSet(ds, targInstalled).getAssignedTargets(); + targInstalled = sendUpdateActionStatusToTargets(ds, targInstalled, Status.FINISHED, "installed"); + + final Slice result = targetManagement.findTargetsAllOrderByLinkedDistributionSet(pageReq, ds.getId(), + null, null, null, Boolean.FALSE, null); + + final Comparator byId = (e1, e2) -> Long.compare(e2.getId(), e1.getId()); + + assertThat(result.getNumberOfElements()).isEqualTo(9); + final List expected = new ArrayList(); + Collections.sort(targInstalled, byId); + Collections.sort(targAssigned, byId); + Collections.sort(notAssigned, byId); + expected.addAll(targInstalled); + expected.addAll(targAssigned); + expected.addAll(notAssigned); + + assertThat(result.getContent()).containsExactly(expected.toArray(new Target[0])); + + } + + private List sendUpdateActionStatusToTargets(final DistributionSet dsA, final Iterable targs, + final Status status, final String... msgs) { + final List result = new ArrayList(); + for (final Target t : targs) { + final List findByTarget = actionRepository.findByTarget(t); + for (final Action action : findByTarget) { + result.add(sendUpdateActionStatusToTarget(status, action, t, msgs)); + } + } + return result; + } + + private Target sendUpdateActionStatusToTarget(final Status status, final Action updActA, final Target t, + final String... msgs) { + updActA.setStatus(status); + + final ActionStatus statusMessages = new ActionStatus(); + statusMessages.setAction(updActA); + statusMessages.setOccurredAt(System.currentTimeMillis()); + statusMessages.setStatus(status); + for (final String msg : msgs) { + statusMessages.addMessage(msg); + } + controllerManagament.addUpdateActionStatus(statusMessages, updActA); + return targetManagement.findTargetByControllerID(t.getControllerId()); + } + +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/TargetManagementTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/TargetManagementTest.java new file mode 100755 index 000000000..4bc6d6f1a --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/TargetManagementTest.java @@ -0,0 +1,632 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.persistence.Query; + +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.WithUser; +import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.exception.TenantNotExistException; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.Tag; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetIdName; +import org.eclipse.hawkbit.repository.model.TargetInfo; +import org.eclipse.hawkbit.repository.model.TargetTag; +import org.junit.Test; +import org.springframework.data.domain.PageRequest; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +import com.google.common.collect.Iterables; + +@Features("Component Tests - Repository") +@Stories("Target Management") +public class TargetManagementTest extends AbstractIntegrationTest { + + @Test + @Description("Ensures that targets cannot be created e.g. in plug'n play scenarios when tenant does not exists.") + @WithUser(tenantId = "tenantWhichDoesNotExists", allSpPermissions = true, autoCreateTenant = false) + public void createTargetForTenantWhichDoesNotExistThrowsTenantNotExistException() { + try { + targetManagement.createTarget(new Target("targetId123")); + fail("tenant not exist"); + } catch (final TenantNotExistException e) { + } + } + + @Test + @Description("Ensures that targets can deleted e.g. test all cascades") + public void deleteAndCreateTargets() { + Target target = targetManagement.createTarget(new Target("targetId123")); + assertThat(targetManagement.countTargetsAll()).isEqualTo(1); + targetManagement.deleteTargets(target.getId()); + assertThat(targetManagement.countTargetsAll()).isEqualTo(0); + + target = createTargetWithAttributes("4711"); + assertThat(targetManagement.countTargetsAll()).isEqualTo(1); + targetManagement.deleteTargets(target.getId()); + assertThat(targetManagement.countTargetsAll()).isEqualTo(0); + + final List targets = new ArrayList(); + for (int i = 0; i < 5; i++) { + target = targetManagement.createTarget(new Target("" + i)); + targets.add(target.getId()); + targets.add(createTargetWithAttributes("" + (i * i + 1000)).getId()); + } + assertThat(targetManagement.countTargetsAll()).isEqualTo(10); + targetManagement.deleteTargets(targets.toArray(new Long[targets.size()])); + assertThat(targetManagement.countTargetsAll()).isEqualTo(0); + } + + private Target createTargetWithAttributes(final String controllerId) { + Target target = new Target(controllerId); + final Map testData = new HashMap<>(); + testData.put("test1", "testdata1"); + + target = targetManagement.createTarget(target); + target = controllerManagament.updateControllerAttributes(controllerId, testData); + + target = targetManagement.findTargetByControllerIDWithDetails(controllerId); + assertThat(target.getTargetInfo().getControllerAttributes()).isEqualTo(testData); + return target; + } + + @Test + @Description("Finds a target by given ID and checks if all data is in the reponse (including the data defined as lazy).") + public void findTargetByControllerIDWithDetails() { + final DistributionSet set = TestDataUtil.generateDistributionSet("test", softwareManagement, + distributionSetManagement); + final DistributionSet set2 = TestDataUtil.generateDistributionSet("test2", softwareManagement, + distributionSetManagement); + + assertThat(targetManagement.countTargetByAssignedDistributionSet(set.getId())).isEqualTo(0); + assertThat(targetManagement.countTargetByInstalledDistributionSet(set.getId())).isEqualTo(0); + assertThat(targetManagement.countTargetByAssignedDistributionSet(set2.getId())).isEqualTo(0); + assertThat(targetManagement.countTargetByInstalledDistributionSet(set2.getId())).isEqualTo(0); + + Target target = createTargetWithAttributes("4711"); + + final long current = System.currentTimeMillis(); + controllerManagament.updateLastTargetQuery("4711", null); + + final DistributionSetAssignmentResult result = deploymentManagement.assignDistributionSet(set.getId(), "4711"); + + final Action action = result.getActions().get(0); + action.setStatus(Status.FINISHED); + controllerManagament.addUpdateActionStatus(new ActionStatus(action, Status.FINISHED, + System.currentTimeMillis(), "message"), action); + deploymentManagement.assignDistributionSet(set2.getId(), "4711"); + + target = targetManagement.findTargetByControllerIDWithDetails("4711"); + // read data + + assertThat(targetManagement.countTargetByAssignedDistributionSet(set.getId())).isEqualTo(0); + assertThat(targetManagement.countTargetByInstalledDistributionSet(set.getId())).isEqualTo(1); + assertThat(targetManagement.countTargetByAssignedDistributionSet(set2.getId())).isEqualTo(1); + assertThat(targetManagement.countTargetByInstalledDistributionSet(set2.getId())).isEqualTo(0); + assertThat(target.getTargetInfo().getLastTargetQuery()).isGreaterThanOrEqualTo(current); + assertThat(target.getAssignedDistributionSet()).isEqualTo(set2); + assertThat(target.getTargetInfo().getInstalledDistributionSet().getId()).isEqualTo(set.getId()); + + } + + @Test + @Description("Checks if the EntityAlreadyExistsException is thrown if the targets with the same controller ID are created twice.") + public void createMultipleTargetsDuplicate() { + final List targets = TestDataUtil.buildTargetFixtures(5, "mySimpleTargs", "my simple targets"); + targetManagement.createTargets(targets); + try { + targetManagement.createTargets(targets); + fail("Targets already exists"); + } catch (final EntityAlreadyExistsException e) { + } + + } + + @Test + @Description("Checks if the EntityAlreadyExistsException is thrown if a single target with the same controller ID are created twice.") + public void createTargetDuplicate() { + targetManagement.createTarget(new Target("4711")); + try { + targetManagement.createTarget(new Target("4711")); + fail("Target already exists"); + } catch (final EntityAlreadyExistsException e) { + } + } + + /** + * verifies, that all {@link TargetTag} of parameter. NOTE: it's accepted + * that the target have additional tags assigned to them which are not + * contained within parameter tags. + * + * @param strict + * if true, the given targets MUST contain EXACTLY ALL given + * tags, AND NO OTHERS. If false, the given targets MUST contain + * ALL given tags, BUT MAY CONTAIN FURTHER ONE + * @param targets + * targets to be verified + * @param tags + * are contained within tags of all targets. + * @param tags + * to be found in the tags of the targets + */ + private void checkTargetHasTags(final boolean strict, final Iterable targets, final TargetTag... tags) { + _target: for (final Target tl : targets) { + final Target t = targetManagement.findTargetByControllerID(tl.getControllerId()); + + for (final Tag tt : t.getTags()) { + for (final Tag tag : tags) { + if (tag.getName().equals(tt.getName())) { + continue _target; + } + } + if (strict) { + fail(); + } + } + fail(); + } + } + + private void checkTargetHasNotTags(final Iterable targets, final TargetTag... tags) { + for (final Target tl : targets) { + final Target t = targetManagement.findTargetByControllerID(tl.getControllerId()); + + for (final Tag tag : tags) { + for (final Tag tt : t.getTags()) { + if (tag.getName().equals(tt.getName())) { + fail(); + } + } + } + } + } + + @Test + @WithUser(allSpPermissions = true) + @Description("Creates and updates a target and verifies the changes in the repository.") + public void singleTargetIsInsertedIntoRepo() throws Exception { + + final String myCtrlID = "myCtrlID"; + final Target target = TestDataUtil.buildTargetFixture(myCtrlID, "the description!"); + + Target savedTarget = targetManagement.createTarget(target); + assertNotNull(savedTarget); + final Long createdAt = savedTarget.getCreatedAt(); + Long modifiedAt = savedTarget.getLastModifiedAt(); + assertEquals(createdAt, modifiedAt); + assertNotNull(savedTarget.getCreatedAt()); + assertNotNull(savedTarget.getLastModifiedAt()); + assertEquals(target, savedTarget); + + savedTarget.setDescription("changed description"); + Thread.sleep(1); + savedTarget = targetManagement.updateTarget(savedTarget); + + assertNotNull(savedTarget.getLastModifiedAt()); + assertNotEquals(createdAt, savedTarget.getLastModifiedAt()); + assertNotEquals(modifiedAt, savedTarget.getLastModifiedAt()); + modifiedAt = savedTarget.getLastModifiedAt(); + + final Target foundTarget = targetManagement.findTargetByControllerID(savedTarget.getControllerId()); + + assertNotNull(foundTarget); + assertEquals(myCtrlID, foundTarget.getControllerId()); + assertEquals(savedTarget, foundTarget); + assertEquals(createdAt, foundTarget.getCreatedAt()); + assertEquals(modifiedAt, foundTarget.getLastModifiedAt()); + } + + @Test + @WithUser(allSpPermissions = true) + @Description("Create multiple tragets as bulk operation and delete them in bulk.") + public void bulkTargetCreationAndDelete() throws Exception { + final String myCtrlID = "myCtrlID"; + final List firstList = TestDataUtil.buildTargetFixtures(100, myCtrlID, "first description"); + + final Target extra = TestDataUtil.buildTargetFixture("myCtrlID-00081XX", "first description"); + + List firstSaved = targetManagement.createTargets(firstList); + + final Target savedExtra = targetManagement.createTarget(extra); + + Iterable allFound = targetRepository.findAll(); + assertEquals(firstList.size(), firstSaved.spliterator().getExactSizeIfKnown()); + assertEquals(firstList.size() + 1, allFound.spliterator().getExactSizeIfKnown()); + + // change the objects and save to again to trigger a change on + // lastModifiedAt + firstSaved.forEach(t -> t.setName(t.getName().concat("\tchanged"))); + firstSaved = targetManagement.updateTargets(firstSaved); + + // verify that all entries are found + _founds: for (final Target foundTarget : allFound) { + for (final Target changedTarget : firstSaved) { + if (changedTarget.getControllerId().equals(foundTarget.getControllerId())) { + assertEquals(changedTarget.getDescription(), foundTarget.getDescription()); + assertTrue(changedTarget.getName().startsWith(foundTarget.getName())); + assertTrue(changedTarget.getName().endsWith("changed")); + assertEquals(changedTarget.getCreatedAt(), foundTarget.getCreatedAt()); + assertThat(changedTarget.getLastModifiedAt()).isNotEqualTo(changedTarget.getCreatedAt()); + + continue _founds; + } + } + + if (!foundTarget.getControllerId().equals(savedExtra.getControllerId())) { + fail(); + } + } + + targetManagement.deleteTargets(savedExtra.getId()); + + final int nr2Del = 50; + int i = nr2Del; + final Long[] deletedTargetIDs = new Long[nr2Del]; + final Target[] deletedTargets = new Target[nr2Del]; + + final Iterator it = firstSaved.iterator(); + while (nr2Del > 0 && it.hasNext() && i > 0) { + final Target pt = it.next(); + deletedTargetIDs[i - 1] = pt.getId(); + deletedTargets[i - 1] = pt; + i--; + } + + targetManagement.deleteTargets(deletedTargetIDs); + + allFound = targetManagement.findTargetsAll(new PageRequest(0, 200)).getContent(); + assertEquals(firstSaved.spliterator().getExactSizeIfKnown() - nr2Del, allFound.spliterator() + .getExactSizeIfKnown()); + + // verify that all undeleted are still found + assertThat(allFound).doesNotContain(deletedTargets); + } + + @Test + @Description("Stores target attributes and verfies existence in the repository.") + public void savingTargetControllerAttributes() { + Iterable ts = targetManagement.createTargets(TestDataUtil.buildTargetFixtures(100, "myCtrlID", + "first description")); + + final Map attribs = new HashMap(); + attribs.put("a.b.c", "abc"); + attribs.put("x.y.z", ""); + attribs.put("1.2.3", "123"); + attribs.put("1.2.3.4", "1234"); + attribs.put("1.2.3.4.5", "12345"); + final Set attribs2Del = new HashSet(); + attribs2Del.add("x.y.z"); + attribs2Del.add("1.2.3"); + + for (final Target t : ts) { + TargetInfo targetInfo = t.getTargetInfo(); + targetInfo.setNew(false); + for (final Entry attrib : attribs.entrySet()) { + final String key = attrib.getKey(); + final String value = String.format("%s-%s", attrib.getValue(), t.getControllerId()); + targetInfo.getControllerAttributes().put(key, value); + } + targetInfo = targetInfoRepository.save(targetInfo); + } + final Query qry = entityManager.createNativeQuery("select * from sp_target_attributes ta"); + final List result = qry.getResultList(); + assertEquals(attribs.size() * ts.spliterator().getExactSizeIfKnown(), result.size()); + + for (final Target myT : ts) { + final Target t = targetManagement.findTargetByControllerIDWithDetails(myT.getControllerId()); + assertEquals(attribs.size(), t.getTargetInfo().getControllerAttributes().size()); + for (final Entry ca : t.getTargetInfo().getControllerAttributes().entrySet()) { + assertTrue(attribs.containsKey(ca.getKey())); + // has the same value: see string concatenation above + assertEquals(String.format("%s-%s", attribs.get(ca.getKey()), t.getControllerId()), ca.getValue()); + } + } + + ts = targetManagement.findTargetsAll(new PageRequest(0, 100)).getContent(); + final Iterator tsIt = ts.iterator(); + // all attributs of the target are deleted + final Target[] ts2DelAllAttribs = new Target[] { tsIt.next(), tsIt.next(), tsIt.next() }; + // a few attributs are deleted + final Target[] ts2DelAttribs = new Target[] { tsIt.next(), tsIt.next() }; + + // perform the deletion operations accordingly + for (final Target ta : ts2DelAllAttribs) { + final Target t = targetManagement.findTargetByControllerIDWithDetails(ta.getControllerId()); + + final TargetInfo targetStatus = t.getTargetInfo(); + targetStatus.getControllerAttributes().clear(); + targetInfoRepository.save(targetStatus); + } + + for (final Target ta : ts2DelAttribs) { + final Target t = targetManagement.findTargetByControllerIDWithDetails(ta.getControllerId()); + + final TargetInfo targetStatus = t.getTargetInfo(); + for (final String attribKey : attribs2Del) { + targetStatus.getControllerAttributes().remove(attribKey); + } + targetInfoRepository.save(targetStatus); + } + + // only the number of the remaining targets and controller attributes + // are checked + final Iterable restTS = targetRepository.findAll(); + + restTarget_: for (final Target targetl : restTS) { + final Target target = targetManagement.findTargetByControllerIDWithDetails(targetl.getControllerId()); + + // verify that all members of the list ts2DelAllAttribs don't have + // any attributes + for (final Target tNoAttribl : ts2DelAllAttribs) { + final Target tNoAttrib = targetManagement.findTargetByControllerID(tNoAttribl.getControllerId()); + + if (tNoAttrib.getControllerId().equals(target.getControllerId())) { + assertThat(target.getTargetInfo().getControllerAttributes()).isEmpty(); + continue restTarget_; + } + } + // verify that that the attribute list of all members of the list + // ts2DelAttribs don't have + // attributes which have been deleted + for (final Target tNoAttribl : ts2DelAttribs) { + final Target tNoAttrib = targetManagement.findTargetByControllerID(tNoAttribl.getControllerId()); + + if (tNoAttrib.getControllerId().equals(target.getControllerId())) { + assertThat(target.getTargetInfo().getControllerAttributes().keySet().toArray()).doesNotContain( + attribs2Del.toArray()); + continue restTarget_; + } + } + } + } + + @Test + @Description("Tests the assigment of tags to the a single target.") + public void targetTagAssignment() { + Target t1 = TestDataUtil.buildTargetFixture("id-1", "blablub"); + final int noT2Tags = 4; + final int noT1Tags = 3; + final List t1Tags = tagManagement.createTargetTags(TestDataUtil.buildTargetTagFixtures(noT1Tags, + "tag1")); + t1.getTags().addAll(t1Tags); + t1 = targetManagement.createTarget(t1); + + Target t2 = TestDataUtil.buildTargetFixture("id-2", "blablub"); + final List t2Tags = tagManagement.createTargetTags(TestDataUtil.buildTargetTagFixtures(noT2Tags, + "tag2")); + t2.getTags().addAll(t2Tags); + t2 = targetManagement.createTarget(t2); + + t1 = targetManagement.findTargetByControllerID(t1.getControllerId()); + assertThat(t1.getTags()).hasSize(noT1Tags).containsAll(t1Tags); + assertThat(t1.getTags()).hasSize(noT1Tags).doesNotContain(Iterables.toArray(t2Tags, TargetTag.class)); + + t2 = targetManagement.findTargetByControllerID(t2.getControllerId()); + assertThat(t2.getTags()).hasSize(noT2Tags).containsAll(t2Tags); + assertThat(t2.getTags()).hasSize(noT2Tags).doesNotContain(Iterables.toArray(t1Tags, TargetTag.class)); + } + + @Test + @Description("Tests the assigment of tags to multiple targets.") + public void targetTagBulkAssignments() { + final List tagATargets = targetManagement.createTargets(TestDataUtil.buildTargetFixtures(10, + "tagATargets", "first description")); + final List tagBTargets = targetManagement.createTargets(TestDataUtil.buildTargetFixtures(10, + "tagBTargets", "first description")); + final List tagCTargets = targetManagement.createTargets(TestDataUtil.buildTargetFixtures(10, + "tagCTargets", "first description")); + + final List tagABTargets = targetManagement.createTargets(TestDataUtil.buildTargetFixtures(10, + "tagABTargets", "first description")); + + final List tagABCTargets = targetManagement.createTargets(TestDataUtil.buildTargetFixtures(10, + "tagABCTargets", "first description")); + + final TargetTag tagA = tagManagement.createTargetTag(new TargetTag("A")); + final TargetTag tagB = tagManagement.createTargetTag(new TargetTag("B")); + final TargetTag tagC = tagManagement.createTargetTag(new TargetTag("C")); + final TargetTag tagX = tagManagement.createTargetTag(new TargetTag("X")); + + // doing different assignments + targetManagement.toggleTagAssignment(tagATargets, tagA); + targetManagement.toggleTagAssignment(tagBTargets, tagB); + targetManagement.toggleTagAssignment(tagCTargets, tagC); + + targetManagement.toggleTagAssignment(tagABTargets, tagA); + targetManagement.toggleTagAssignment(tagABTargets, tagB); + + targetManagement.toggleTagAssignment(tagABCTargets, tagA); + targetManagement.toggleTagAssignment(tagABCTargets, tagB); + targetManagement.toggleTagAssignment(tagABCTargets, tagC); + + assertThat(targetManagement.countTargetByFilters(null, null, null, Boolean.FALSE, "X")).isEqualTo(0); + + // search for targets with tag tagA + final List targetWithTagA = new ArrayList(); + final List targetWithTagB = new ArrayList(); + final List targetWithTagC = new ArrayList(); + + // storing target lists to enable easy evaluation + Iterables.addAll(targetWithTagA, tagATargets); + Iterables.addAll(targetWithTagA, tagABTargets); + Iterables.addAll(targetWithTagA, tagABCTargets); + + Iterables.addAll(targetWithTagB, tagBTargets); + Iterables.addAll(targetWithTagB, tagABTargets); + Iterables.addAll(targetWithTagB, tagABCTargets); + + Iterables.addAll(targetWithTagC, tagCTargets); + Iterables.addAll(targetWithTagC, tagABCTargets); + + // check the target lists as returned by assignTag + checkTargetHasTags(false, targetWithTagA, tagA); + checkTargetHasTags(false, targetWithTagB, tagB); + checkTargetHasTags(false, targetWithTagC, tagC); + + checkTargetHasNotTags(tagATargets, tagB, tagC); + checkTargetHasNotTags(tagBTargets, tagA, tagC); + checkTargetHasNotTags(tagCTargets, tagA, tagB); + + // check again target lists refreshed from DB + assertThat(targetManagement.countTargetByFilters(null, null, null, Boolean.FALSE, "A")).isEqualTo( + targetWithTagA.size()); + assertThat(targetManagement.countTargetByFilters(null, null, null, Boolean.FALSE, "B")).isEqualTo( + targetWithTagB.size()); + assertThat(targetManagement.countTargetByFilters(null, null, null, Boolean.FALSE, "C")).isEqualTo( + targetWithTagC.size()); + } + + @Test + @Description("Tests the unassigment of tags to multiple targets.") + public void targetTagBulkUnassignments() { + final TargetTag targTagA = tagManagement.createTargetTag(new TargetTag("Targ-A-Tag")); + final TargetTag targTagB = tagManagement.createTargetTag(new TargetTag("Targ-B-Tag")); + final TargetTag targTagC = tagManagement.createTargetTag(new TargetTag("Targ-C-Tag")); + + final List targAs = targetManagement.createTargets(TestDataUtil.buildTargetFixtures(25, "target-id-A", + "first description")); + final List targBs = targetManagement.createTargets(TestDataUtil.buildTargetFixtures(20, "target-id-B", + "first description")); + final List targCs = targetManagement.createTargets(TestDataUtil.buildTargetFixtures(15, "target-id-C", + "first description")); + + final List targABs = targetManagement.createTargets(TestDataUtil.buildTargetFixtures(12, + "target-id-AB", "first description")); + final List targACs = targetManagement.createTargets(TestDataUtil.buildTargetFixtures(13, + "target-id-AC", "first description")); + final List targBCs = targetManagement.createTargets(TestDataUtil.buildTargetFixtures(7, "target-id-BC", + "first description")); + final List targABCs = targetManagement.createTargets(TestDataUtil.buildTargetFixtures(17, + "target-id-ABC", "first description")); + + targetManagement.toggleTagAssignment(targAs, targTagA); + targetManagement.toggleTagAssignment(targABs, targTagA); + targetManagement.toggleTagAssignment(targACs, targTagA); + targetManagement.toggleTagAssignment(targABCs, targTagA); + + targetManagement.toggleTagAssignment(targBs, targTagB); + targetManagement.toggleTagAssignment(targABs, targTagB); + targetManagement.toggleTagAssignment(targBCs, targTagB); + targetManagement.toggleTagAssignment(targABCs, targTagB); + + targetManagement.toggleTagAssignment(targCs, targTagC); + targetManagement.toggleTagAssignment(targACs, targTagC); + targetManagement.toggleTagAssignment(targBCs, targTagC); + targetManagement.toggleTagAssignment(targABCs, targTagC); + + checkTargetHasTags(true, targAs, targTagA); + checkTargetHasTags(true, targBs, targTagB); + checkTargetHasTags(true, targABs, targTagA, targTagB); + checkTargetHasTags(true, targACs, targTagA, targTagC); + checkTargetHasTags(true, targBCs, targTagB, targTagC); + checkTargetHasTags(true, targABCs, targTagA, targTagB, targTagC); + + targetManagement.toggleTagAssignment(targCs, targTagC); + targetManagement.toggleTagAssignment(targACs, targTagC); + targetManagement.toggleTagAssignment(targBCs, targTagC); + targetManagement.toggleTagAssignment(targABCs, targTagC); + + checkTargetHasTags(true, targAs, targTagA); + checkTargetHasTags(true, targBs, targTagB); + checkTargetHasTags(true, targABs, targTagA, targTagB); + checkTargetHasTags(true, targBCs, targTagB); + checkTargetHasTags(true, targACs, targTagA); + + checkTargetHasNotTags(targCs, targTagC); + checkTargetHasNotTags(targACs, targTagC); + checkTargetHasNotTags(targBCs, targTagC); + checkTargetHasNotTags(targABCs, targTagC); + + } + + @Test + @Description("Retrieves targets by ID with lazy loading of the tags. Checks the successfull load.") + public void findTargetsByControllerIDsWithTags() { + final TargetTag targTagA = tagManagement.createTargetTag(new TargetTag("Targ-A-Tag")); + + final List targAs = targetManagement.createTargets(TestDataUtil.buildTargetFixtures(25, "target-id-A", + "first description")); + + targetManagement.toggleTagAssignment(targAs, targTagA); + + assertThat( + targetManagement.findTargetsByControllerIDsWithTags(targAs.stream() + .map(target -> target.getControllerId()).collect(Collectors.toList()))).hasSize(25); + + // no lazy loading exception and tag correctly assigned + assertThat( + targetManagement + .findTargetsByControllerIDsWithTags( + targAs.stream().map(target -> target.getControllerId()).collect(Collectors.toList())) + .stream().map(target -> target.getTags().contains(targTagA)).collect(Collectors.toList())) + .containsOnly(true); + } + + @Test + @Description("Test the optimized quere for retrieving all ID/name pairs of targets.") + public void findAllTargetIdNamePaiss() { + final List targAs = targetManagement.createTargets(TestDataUtil.buildTargetFixtures(25, "target-id-A", + "first description")); + final String[] createdTargetIds = targAs.stream().map(t -> t.getControllerId()) + .toArray(size -> new String[size]); + + final List findAllTargetIdNames = targetManagement.findAllTargetIds(); + final List findAllTargetIds = findAllTargetIdNames.stream().map(TargetIdName::getControllerId) + .collect(Collectors.toList()); + + assertThat(findAllTargetIds).containsOnly(createdTargetIds); + } + + @Test + @Description("Test that NO TAG functionality which gives all targets with no tag assigned.") + public void findTargetsWithNoTag() { + + final TargetTag targTagA = tagManagement.createTargetTag(new TargetTag("Targ-A-Tag")); + final List targAs = targetManagement.createTargets(TestDataUtil.buildTargetFixtures(25, "target-id-A", + "first description")); + targetManagement.toggleTagAssignment(targAs, targTagA); + + targetManagement.createTargets(TestDataUtil.buildTargetFixtures(25, "target-id-B", "first description")); + + final String[] tagNames = null; + final List targetsListWithNoTag = targetManagement.findTargetByFilters(new PageRequest(0, 500), null, + null, null, Boolean.TRUE, tagNames).getContent(); + + // Total targets + assertEquals(50, targetManagement.findAllTargetIds().size()); + // Targets with no tag + assertEquals(25, targetsListWithNoTag.size()); + } +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/utils/RepositoryDataGenerator.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/utils/RepositoryDataGenerator.java new file mode 100755 index 000000000..dffec4df1 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/repository/utils/RepositoryDataGenerator.java @@ -0,0 +1,490 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.utils; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.List; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import javax.transaction.Transactional; + +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails; +import org.eclipse.hawkbit.repository.ControllerManagement; +import org.eclipse.hawkbit.repository.DeploymentManagement; +import org.eclipse.hawkbit.repository.DistributionSetAssignmentResult; +import org.eclipse.hawkbit.repository.DistributionSetManagement; +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.repository.TagManagement; +import org.eclipse.hawkbit.repository.TargetManagement; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetTag; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetTag; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.eclipse.hawkbit.util.IpUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.data.auditing.AuditingHandler; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; + +import net._01001111.text.LoremIpsum; + +/** + * Generates test data for setting up the repository for test or demonstration + * purpose. + * + * + * + */ +public final class RepositoryDataGenerator { + private static final Logger LOG = LoggerFactory.getLogger(RepositoryDataGenerator.class); + + public static void initDemoRepo(final ConfigurableApplicationContext context) { + final PersistentInitDemoDataBuilder initDemoDataBuilder = new PersistentInitDemoDataBuilder(context); + initDemoDataBuilder.initDemoRepo(20, 0); + } + + public static void initLoadRepo(final ConfigurableApplicationContext context) { + final PersistentInitDemoDataBuilder initDemoDataBuilder = new PersistentInitDemoDataBuilder(context); + initDemoDataBuilder.initDemoRepo(200, 200); + } + + /** + * builder which build initial demo data and stores it to the repos. + * + */ + private static final class PersistentInitDemoDataBuilder { + + private final SoftwareManagement softwareManagement; + private final TargetManagement targetManagement; + private final DeploymentManagement deploymentManagement; + private final TagManagement tagManagement; + private final ControllerManagement controllerManagement; + private final DistributionSetManagement distributionSetManagement; + + private final DatabaseCleanupUtil dbCleanupUtil; + + private final AuditingHandler auditingHandler; + + final LoremIpsum jlorem = new LoremIpsum(); + + PersistentInitDemoDataBuilder(final ConfigurableApplicationContext context) { + softwareManagement = context.getBean(SoftwareManagement.class); + targetManagement = context.getBean(TargetManagement.class); + tagManagement = context.getBean(TagManagement.class); + deploymentManagement = context.getBean(DeploymentManagement.class); + controllerManagement = context.getBean(ControllerManagement.class); + distributionSetManagement = context.getBean(DistributionSetManagement.class); + + dbCleanupUtil = context.getBean(DatabaseCleanupUtil.class); + + auditingHandler = context.getBean(AuditingHandler.class); + } + + private void runAsAllAuthorityContext(final Runnable runnable) { + final SecurityContext oldContext = SecurityContextHolder.getContext(); + try { + final SecurityContextImpl securityContextImpl = new SecurityContextImpl(); + final TestingAuthenticationToken authentication = new TestingAuthenticationToken("repogenator", + "repogenator", SpPermission.CREATE_REPOSITORY, SpPermission.CREATE_TARGET, + SpPermission.DELETE_REPOSITORY, SpPermission.DELETE_TARGET, SpPermission.READ_REPOSITORY, + SpPermission.READ_TARGET, SpPermission.UPDATE_REPOSITORY, SpPermission.UPDATE_TARGET, + SpringEvalExpressions.CONTROLLER_ROLE); + securityContextImpl.setAuthentication(authentication); + authentication.setDetails(new TenantAwareAuthenticationDetails("default", false)); + SecurityContextHolder.setContext(securityContextImpl); + runnable.run(); + } finally { + SecurityContextHolder.setContext(oldContext); + } + } + + public void generateTestTagetGroup(final String group, final int sizeMultiplikator) { + final DistributionSetTag dsTag = tagManagement + .createDistributionSetTag(new DistributionSetTag("For " + group + "s")); + + auditingHandler.setDateTimeProvider(new DateTimeProvider() { + @Override + public Calendar getNow() { + final Calendar instance = Calendar.getInstance(); + instance.add(Calendar.MONTH, -new Random().nextInt(7)); + + return instance; + } + }); + + final List targets = createTargetTestGroup(group, 20 * sizeMultiplikator); + + auditingHandler.setDateTimeProvider(new DateTimeProvider() { + @Override + public Calendar getNow() { + final Calendar instance = Calendar.getInstance(); + return instance; + } + }); + + LOG.debug("initDemoRepo - start now real action history for group: {}", group); + + // Old history of succesfully finished operations + IntStream.range(0, 10).forEach(idx -> { + final DistributionSet dsReal = TestDataUtil.generateDistributionSet(group + " Release", "v1." + idx, + softwareManagement, distributionSetManagement, + Arrays.asList(new DistributionSetTag[] { dsTag })); + + final DistributionSetAssignmentResult result = deploymentManagement.assignDistributionSet(dsReal, + targets); + + createSimpleActionStatusHistory(result.getActions()); + }); + + final List> targetGroups = splitIntoGroups(targets); + + IntStream.range(0, targetGroups.size()).forEach(idx -> { + final DistributionSet dsReal = TestDataUtil.generateDistributionSet(group + " Release", "v2." + idx, + softwareManagement, distributionSetManagement, + Arrays.asList(new DistributionSetTag[] { dsTag })); + + final DistributionSetAssignmentResult result = deploymentManagement.assignDistributionSet(dsReal, + targetGroups.get(idx)); + + createActionStatusHistory(result.getActions(), sizeMultiplikator); + }); + + LOG.debug("initDemoRepo - real action history finished for group: {}", group); + } + + private List> splitIntoGroups(final List allTargets) { + + final int elements = allTargets.size(); + + final int group1 = elements * (5 + new Random().nextInt(5)) / 100; + final int group2 = elements * (15 + new Random().nextInt(5)) / 100; + + final List> result = new ArrayList>(); + + result.add(allTargets.subList(0, group1)); + result.add(allTargets.subList(group1, group2)); + result.add(allTargets.subList(group2, elements)); + + return result; + + } + + private void createActionStatusHistory(final List actions, final int sizeMultiplikator) { + final AtomicInteger counter = new AtomicInteger(); + + int index = 0; + for (final Action actionGiven : actions) { + // retrieved + Action action = controllerManagement.registerRetrieved(actionGiven, + "Controller retrieved update action and should start now the download."); + + // download + final ActionStatus download = new ActionStatus(); + download.setAction(action); + download.setStatus(Status.DOWNLOAD); + download.addMessage("Controller started download."); + action = controllerManagement.addUpdateActionStatus(download, action); + + // warning + final ActionStatus warning = new ActionStatus(); + warning.setAction(action); + warning.setStatus(Status.WARNING); + warning.addMessage("Some warning: " + jlorem.words(new Random().nextInt(50))); + action = controllerManagement.addUpdateActionStatus(warning, action); + + // garbage + for (int i = 0; i < new Random().nextInt(10); i++) { + final ActionStatus running = new ActionStatus(); + running.setAction(action); + running.setStatus(Status.RUNNING); + running.addMessage("Still running: " + jlorem.words(new Random().nextInt(50))); + action = controllerManagement.addUpdateActionStatus(running, action); + for (int g = 0; g < new Random().nextInt(5); g++) { + final ActionStatus rand = new ActionStatus(); + rand.setAction(action); + rand.setStatus(Status.RUNNING); + rand.addMessage(jlorem.words(new Random().nextInt(50))); + action = controllerManagement.addUpdateActionStatus(rand, action); + } + } + + // close + final ActionStatus close = new ActionStatus(); + close.setAction(action); + + // with error + final int incrementAndGet = counter.incrementAndGet(); + if (incrementAndGet % 5 == 0) { + close.setStatus(Status.ERROR); + close.addMessage("Controller reported CLOSED with ERROR!"); + action = controllerManagement.addUpdateActionStatus(close, action); + } + // with OK + else { + close.setStatus(Status.FINISHED); + close.addMessage("Controller reported CLOSED with OK!"); + action = controllerManagement.addUpdateActionStatus(close, action); + } + + index++; + } + } + + private void createSimpleActionStatusHistory(final List actions) { + for (final Action actionGiven : actions) { + // retrieved + Action action = controllerManagement.registerRetrieved(actionGiven, + "Controller retrieved update action and should start now the download."); + + // close + final ActionStatus close = new ActionStatus(); + close.setAction(action); + close.setStatus(Status.FINISHED); + close.addMessage("Controller reported CLOSED with OK!"); + action = controllerManagement.addUpdateActionStatus(close, action); + + } + } + + private List createTargetTestGroup(final String group, final int targets) { + LOG.debug("createTargetTestGroup: create group {}", group); + + final TargetTag targTag = tagManagement.createTargetTag(new TargetTag(group)); + + final List targAs = targetManagement.createTargets(buildTargets(targets, group), + TargetUpdateStatus.REGISTERED, System.currentTimeMillis() - new Random().nextInt(50_000_000), + generateIPAddress()); + LOG.debug("createTargetTestGroup: {} created", group); + + LOG.debug("createTargetTestGroup: {} targets status updated including IP", group); + + return targetManagement.toggleTagAssignment(targAs, targTag).getAssignedTargets(); + } + + private List buildTargets(final int noOfTgts, final String descriptionPrefix) { + + final List result = new ArrayList(noOfTgts); + + for (int i = 0; i < noOfTgts; i++) { + final Target target = new Target(UUID.randomUUID().toString()); + + final StringBuilder builder = new StringBuilder(); + builder.append(descriptionPrefix); + builder.append(jlorem.words(5)); + + target.setDescription(builder.toString()); + target.getTargetInfo().getControllerAttributes().put("revision", "1.1"); + target.getTargetInfo().getControllerAttributes().put("capacity", "128M"); + target.getTargetInfo().getControllerAttributes().put("serial", + String.valueOf(System.currentTimeMillis())); + + result.add(target); + + } + + return result; + } + + /** + * method writes initial test/demo data to the repositories. + * + * @param sizeMultiplikator + * the entire scenario + * @param loadtestgroups + * packages of 1_000 targets + */ + private void initDemoRepo(final int sizeMultiplikator, final int loadtestgroups) { + final LoremIpsum jlorem = new LoremIpsum(); + + runAsAllAuthorityContext(new Runnable() { + + @Override + public void run() { + dbCleanupUtil.cleanupDB(null); + + // generate targets and assign DS + // 5 groups - 100 targets each -> 500 + final String[] targetTestGroups = { "SHC", "CCU", "Vehicle", "Vending machine", "ECU" }; + + final String[] modulesTypes = { "HeadUnit_FW", "EDC17_FW", "OSGi_Bundle" }; + + final DistributionSetTag depTag = tagManagement + .createDistributionSetTag(new DistributionSetTag("deprecated")); + + Arrays.stream(targetTestGroups).forEach(group -> { + generateTestTagetGroup(group, sizeMultiplikator); + }); + + // garbage DS + LOG.debug("initDemoRepo - start now DS garbage"); + TestDataUtil.generateDistributionSets("Generic Software Package", sizeMultiplikator, + softwareManagement, distributionSetManagement); + LOG.debug("initDemoRepo - DS garbage finished"); + + LOG.debug("initDemoRepo - start now Extra Software Modules and types"); + Arrays.stream(modulesTypes).forEach(typeName -> { + final SoftwareModuleType smtype = softwareManagement.createSoftwareModuleType( + new SoftwareModuleType(typeName.toLowerCase().replaceAll("\\s+", ""), typeName, + jlorem.words(5), Integer.MAX_VALUE)); + + for (int i = 0; i < sizeMultiplikator; i++) { + softwareManagement.createSoftwareModule(new SoftwareModule(smtype, typeName + i, "1.0." + i, + jlorem.words(5), "the " + typeName + " vendor Inc.")); + } + + }); + LOG.debug("initDemoRepo - Extra Software Modules and types finished"); + + LOG.debug("initDemoRepo - start now target garbage"); + + // garbage targets + // unknown + targetManagement + .createTargets(TestDataUtil.generateTargets(targetTestGroups.length * sizeMultiplikator)); + + // registered + targetManagement.createTargets( + TestDataUtil.generateTargets(targetTestGroups.length * sizeMultiplikator, "registered"), + TargetUpdateStatus.REGISTERED, System.currentTimeMillis(), generateIPAddress()); + + // pending + final DistributionSetTag dsTag = tagManagement + .createDistributionSetTag(new DistributionSetTag("OnlyAssignedTag")); + final DistributionSet ds = TestDataUtil.generateDistributionSet("Pending DS", "v1.0", + softwareManagement, distributionSetManagement, + Arrays.asList(new DistributionSetTag[] { dsTag })); + deploymentManagement.assignDistributionSet(ds, targetManagement.createTargets( + TestDataUtil.generateTargets(targetTestGroups.length * sizeMultiplikator, "pending"))); + + // Load test means additional 1_000_000 target + + for (int i = 0; i < loadtestgroups; i++) { + targetManagement.createTargets(TestDataUtil.generateTargets(i * 1_000, 1_000, "loadtest-")); + } + + LOG.debug("initDemoRepo complete"); + } + + }); + } + /** + * Adding controller attributes for given {@link Target}. + */ + // private Target setControllerAttributes( final String targetId ) { + // final Target target = + // targetManagement.findTargetByControllerIDWithDetails( targetId ); + // target.getTargetStatus().getControllerAttributes().put( "revision", + // "1.1" ); + // target.getTargetStatus().getControllerAttributes().put( "capacity", + // "128M" ); + // target.getTargetStatus().getControllerAttributes().put( "serial", + // String.valueOf( + // System.currentTimeMillis()) ); + // return targetManagement.updateTarget( target ); + // } + } + + /** + * + * Data clean up class. + * + * + * + */ + public static class DatabaseCleanupUtil { + + private static final Logger LOG = LoggerFactory.getLogger(DatabaseCleanupUtil.class); + @Autowired + private EntityManager entityManager; + + private static final String[] CLEAN_UP_SQLS = new String[] { "sp_tenant", "sp_tenant_configuration", + "sp_artifact", "sp_external_artifact", "sp_external_provider", "sp_target_target_tag", + "sp_target_attributes", "sp_target_tag", "sp_action_status_messages", "sp_action_status", "sp_action", + "sp_ds_dstag", "sp_distributionset_tag", "sp_target_info", "sp_target", "sp_sw_metadata", + "sp_ds_metadata", "sp_ds_module", "sp_distribution_set", "sp_base_software_module", + "sp_ds_type_element", "sp_distribution_set_type", "sp_software_module_type" }; + + /** + * delete all entries from the DB tables. + */ + @Transactional + @Modifying + public void cleanupDB(final String database) { + LOG.debug("Data clean up is started..."); + final boolean isMySql = "MYSQL".equals(database); + if (isMySql) { + // disable foreign key check because otherwise mysql cannot + // delete sp_active_actions due + // the self constraint within the table, stupid MySql + entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 0").executeUpdate(); + } + try { + final String[] dbCmds = new String[] { "delete from" }; + for (final String dbCmd : dbCmds) { + for (final String table : CLEAN_UP_SQLS) { + final String sql = String.format("%s %s", dbCmd, table); + final Query query = entityManager.createNativeQuery(sql); + try { + LOG.debug("cleanup table: {}", sql); + LOG.debug("cleaned table: {}; deleted {} records", sql, query.executeUpdate()); + } catch (final Exception ex) { + LOG.error(String.format("error on executing cleanup statement '%s'", sql), ex); + throw ex; + } + } + } + + LOG.debug("Data clean up is finished..."); + } finally { + if (isMySql) { + // enable foreign key check again! + entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 1").executeUpdate(); + } + } + } + } + + /** + * @return a generated IPv4 address string. + */ + private static URI generateIPAddress() { + final Random r = new Random(); + return IpUtil + .createHttpUri(r.nextInt(256) + "." + r.nextInt(256) + "." + r.nextInt(256) + "." + r.nextInt(256)); + } + + private RepositoryDataGenerator() { + super(); + } + +} diff --git a/hawkbit-repository/src/test/java/org/eclipse/hawkbit/tenancy/MultiTenancyEntityTest.java b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/tenancy/MultiTenancyEntityTest.java new file mode 100755 index 000000000..51e49ef04 --- /dev/null +++ b/hawkbit-repository/src/test/java/org/eclipse/hawkbit/tenancy/MultiTenancyEntityTest.java @@ -0,0 +1,212 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.tenancy; + +import static org.fest.assertions.api.Assertions.assertThat; + +import java.util.concurrent.Callable; + +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.WithSpringAuthorityRule; +import org.eclipse.hawkbit.WithUser; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.Target; +import org.junit.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Slice; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * Multi-Tenancy tests which testing the CRUD operations of entities that all + * CRUD-Operations are tenant aware and cannot access or delete entities not + * belonging to the current tenant. + * + * + * + * + */ +@Features("Component Tests - Repository") +@Stories("Multi Tenancy") +public class MultiTenancyEntityTest extends AbstractIntegrationTest { + + @Test + @Description(value = "Ensures that multiple targets with same controller-ID can be created for different tenants.") + public void createMultipleTargetsWithSameIdForDifferentTenant() throws Exception { + // known controller ID for overall tenants same + final String knownControllerId = "controllerId"; + + // known tenant names + final String tenant = "aTenant"; + final String anotherTenant = "anotherTenant"; + // create targets + createTargetForTenant(knownControllerId, tenant); + createTargetForTenant(knownControllerId, anotherTenant); + + // ensure both tenants see their target + final Slice findTargetsForTenant = findTargetsForTenant(tenant); + assertThat(findTargetsForTenant).hasSize(1); + assertThat(findTargetsForTenant.getContent().get(0).getTenant().toUpperCase()).isEqualTo(tenant.toUpperCase()); + final Slice findTargetsForAnotherTenant = findTargetsForTenant(anotherTenant); + assertThat(findTargetsForAnotherTenant).hasSize(1); + assertThat(findTargetsForAnotherTenant.getContent().get(0).getTenant().toUpperCase()) + .isEqualTo(anotherTenant.toUpperCase()); + + } + + @Test + @Description(value = "Ensures that targtes created by a tenant are not visible by another tenant.") + @WithUser(tenantId = "mytenant", allSpPermissions = true) + public void queryTargetFromDifferentTenantIsNotVisible() throws Exception { + // create target for another tenant + final String anotherTenant = "anotherTenant"; + final String controllerAnotherTenant = "anotherController"; + createTargetForTenant(controllerAnotherTenant, anotherTenant); + + // find all targets for current tenant "mytenant" + final Slice findTargetsAll = targetManagement.findTargetsAll(pageReq); + // no target has been created for "mytenant" + assertThat(findTargetsAll).hasSize(0); + + // find all targets for anotherTenant + final Slice findTargetsForTenant = findTargetsForTenant(anotherTenant); + // another tenant should have targets + assertThat(findTargetsForTenant).hasSize(1); + } + + @Test + @Description(value = "Ensures that tenant metadata is retrieved for the current tenant.") + @WithUser(tenantId = "mytenant", autoCreateTenant = false, allSpPermissions = true) + public void getTenanatMetdata() throws Exception { + + // logged in tenant mytenant - check if tenant default data is + // autogenerated + assertThat(distributionSetManagement.findDistributionSetTypesAll(pageReq)).isEmpty(); + assertThat(systemManagement.getTenantMetadata().getTenant().toUpperCase()).isEqualTo("mytenant".toUpperCase()); + assertThat(distributionSetManagement.findDistributionSetTypesAll(pageReq)).isNotEmpty(); + + // check that the cache is not getting in the way, i.e. "bumlux" results + // in bumlux and not + // mytenant + assertThat( + securityRule.runAs(WithSpringAuthorityRule.withUserAndTenant("user", "bumlux"), new Callable() { + @Override + public String call() throws Exception { + return systemManagement.getTenantMetadata().getTenant().toUpperCase(); + } + })).isEqualTo("bumlux".toUpperCase()); + } + + @Test + @Description(value = "Ensures that targets created from a different tenant cannot be deleted from other tenants") + @WithUser(tenantId = "mytenant", allSpPermissions = true) + public void deleteTargetFromOtherTenantIsNotPossible() throws Exception { + // create target for another tenant + final String anotherTenant = "anotherTenant"; + final String controllerAnotherTenant = "anotherController"; + final Target createTargetForTenant = createTargetForTenant(controllerAnotherTenant, anotherTenant); + + // ensure target cannot be deleted by 'mytenant' + targetManagement.deleteTargets(createTargetForTenant.getId()); + Slice targetsForAnotherTenant = findTargetsForTenant(anotherTenant); + assertThat(targetsForAnotherTenant).hasSize(1); + + // ensure another tenant can delete the target + deleteTargetsForTenant(anotherTenant, createTargetForTenant.getId()); + targetsForAnotherTenant = findTargetsForTenant(anotherTenant); + assertThat(targetsForAnotherTenant).hasSize(0); + } + + @Test + @Description(value = "Ensures that multiple distribution sets with same name and version can be created for different tenants.") + public void createMultipleDistributionSetsWithSameNameForDifferentTenants() throws Exception { + + // known ds name for overall tenants same + final String knownDistributionSetName = "dsName"; + final String knownDistributionSetVersion = "0.0.0"; + + // known tenant names + final String tenant = "aTenant"; + final String anotherTenant = "anotherTenant"; + // create distribution sets + createDistributionSetForTenant(knownDistributionSetName, knownDistributionSetVersion, tenant); + createDistributionSetForTenant(knownDistributionSetName, knownDistributionSetVersion, anotherTenant); + + // ensure both tenants see their distribution sets + final Page findDistributionSetsForTenant = findDistributionSetForTenant(tenant); + assertThat(findDistributionSetsForTenant).hasSize(1); + assertThat(findDistributionSetsForTenant.getContent().get(0).getTenant().toUpperCase()) + .isEqualTo(tenant.toUpperCase()); + final Page findDistributionSetsForAnotherTenant = findDistributionSetForTenant(anotherTenant); + assertThat(findDistributionSetsForAnotherTenant).hasSize(1); + assertThat(findDistributionSetsForAnotherTenant.getContent().get(0).getTenant().toUpperCase()) + .isEqualTo(anotherTenant.toUpperCase()); + + } + + private Target createTargetForTenant(final String controllerId, final String tenant) throws Exception { + return securityRule.runAs(WithSpringAuthorityRule.withUserAndTenant("user", tenant), new Callable() { + @Override + public Target call() throws Exception { + return targetManagement.createTarget(new Target(controllerId)); + } + }); + } + + private Slice findTargetsForTenant(final String tenant) throws Exception { + return securityRule.runAs(WithSpringAuthorityRule.withUserAndTenant("user", tenant), + new Callable>() { + @Override + public Slice call() throws Exception { + return targetManagement.findTargetsAll(pageReq); + } + }); + } + + private void deleteTargetsForTenant(final String tenant, final Long... targetIds) throws Exception { + securityRule.runAs(WithSpringAuthorityRule.withUserAndTenant("user", tenant), new Callable() { + @Override + public Void call() throws Exception { + targetManagement.deleteTargets(targetIds); + return null; + } + }); + } + + private DistributionSet createDistributionSetForTenant(final String name, final String version, final String tenant) + throws Exception { + return securityRule.runAs(WithSpringAuthorityRule.withUserAndTenant("user", tenant), + new Callable() { + @Override + public DistributionSet call() throws Exception { + final DistributionSet ds = new DistributionSet(); + ds.setName(name); + ds.setTenant(tenant); + ds.setVersion(version); + ds.setType(distributionSetManagement + .createDistributionSetType(new DistributionSetType("typetest", "test", "foobar"))); + return distributionSetManagement.createDistributionSet(ds); + } + }); + } + + private Page findDistributionSetForTenant(final String tenant) throws Exception { + return securityRule.runAs(WithSpringAuthorityRule.withUserAndTenant("user", tenant), + new Callable>() { + @Override + public Page call() throws Exception { + return distributionSetManagement.findDistributionSetsAll(pageReq, false, false); + } + }); + } + +} diff --git a/hawkbit-repository/src/test/resources/application-test.properties b/hawkbit-repository/src/test/resources/application-test.properties new file mode 100755 index 000000000..129f2e3e2 --- /dev/null +++ b/hawkbit-repository/src/test/resources/application-test.properties @@ -0,0 +1,38 @@ +# +# Copyright (c) 2015 Bosch Software Innovations GmbH and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# + +spring.data.mongodb.uri=mongodb://localhost/spArtifactRepository${random.value} +spring.data.mongodb.port=28017 + +hawkbit.server.controller.security.authentication.header.enabled=true + +hawkbit.server.artifact.repo.upload.maxFileSize=5MB + +hawkbit.server.security.dos.maxStatusEntriesPerAction=100 + +hawkbit.server.security.dos.maxAttributeEntriesPerTarget=10 + +spring.jpa.database=H2 +flyway.enabled=true +flyway.initOnMigrate=true +flyway.sqlMigrationSuffix=${spring.jpa.database}.sql + +spring.datasource.url=jdbc:h2:mem:sp-db;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=sa + +# SP Controller configuration +hawkbit.controller.pollingTime=00:01:00 +hawkbit.controller.pollingOverdueTime=00:01:00 + +## Configuration for RabbitMQ integration +hawkbit.dmf.rabbitmq.deadLetterQueue=dmf_connector_deadletter +hawkbit.dmf.rabbitmq.deadLetterExchange=dmf.connector.deadletter +hawkbit.dmf.rabbitmq.receiverQueue=dmf_receiver diff --git a/hawkbit-rest-api/pom.xml b/hawkbit-rest-api/pom.xml new file mode 100755 index 000000000..f93f5aa1f --- /dev/null +++ b/hawkbit-rest-api/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + org.eclipse.hawkbit + hawkbit-parent + 0.1.0-SNAPSHOT + + hawkbit-rest-api + hawkBit :: REST API + + + + + javax.validation + validation-api + 1.1.0.Final + + + org.springframework.hateoas + spring-hateoas + + + com.fasterxml.jackson.core + jackson-annotations + + + io.swagger + swagger-annotations + + + \ No newline at end of file diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/BaseEntityRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/BaseEntityRest.java new file mode 100755 index 000000000..21d05cb2e --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/BaseEntityRest.java @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; +import org.springframework.hateoas.ResourceSupport; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModelProperty; + +/** + * A json annotated rest model for BaseEntity to RESTful API representation. + * + * + * + * + */ +public abstract class BaseEntityRest extends ResourceSupport { + + @ApiModelProperty(value = ApiModelProperties.CREATED_BY) + @JsonProperty + private String createdBy; + + @ApiModelProperty(value = ApiModelProperties.CREATED_AT) + @JsonProperty + private Long createdAt; + + @ApiModelProperty(value = ApiModelProperties.LAST_MODIFIED_BY) + @JsonProperty + private String lastModifiedBy; + + @ApiModelProperty(value = ApiModelProperties.LAST_MODIFIED_AT) + @JsonProperty + private Long lastModifiedAt; + + /** + * @return the createdBy + */ + public String getCreatedBy() { + return createdBy; + } + + /** + * @param createdBy + * the createdBy to set + */ + @JsonIgnore + public void setCreatedBy(final String createdBy) { + this.createdBy = createdBy; + } + + /** + * @return the createdAt + */ + public Long getCreatedAt() { + return createdAt; + } + + /** + * @param createdAt + * the createdAt to set + */ + @JsonIgnore + public void setCreatedAt(final Long createdAt) { + this.createdAt = createdAt; + } + + /** + * @return the lastModifiedBy + */ + public String getLastModifiedBy() { + return lastModifiedBy; + } + + /** + * @param lastModifiedBy + * the lastModifiedBy to set + */ + @JsonIgnore + public void setLastModifiedBy(final String lastModifiedBy) { + this.lastModifiedBy = lastModifiedBy; + } + + /** + * @return the lastModifiedAt + */ + public Long getLastModifiedAt() { + return lastModifiedAt; + } + + /** + * @param lastModifiedAt + * the lastModifiedAt to set + */ + @JsonIgnore + public void setLastModifiedAt(final Long lastModifiedAt) { + this.lastModifiedAt = lastModifiedAt; + } +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/ExceptionInfo.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/ExceptionInfo.java new file mode 100755 index 000000000..6543c4259 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/ExceptionInfo.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * A exception model rest representation with JSON annotations for response + * bodies in case of RESTful exception occurrence. + * + * + * + */ +@JsonInclude(Include.NON_EMPTY) +@ApiModel(value = "ExceptionInfo") +public class ExceptionInfo { + + @ApiModelProperty(required = true, value = "The exception class name") + private String exceptionClass; + @ApiModelProperty(required = true, value = "The exception error code") + private String errorCode; + @ApiModelProperty(required = false, value = "The exception human readable message") + private String message; + @ApiModelProperty(required = false, value = "The exception message parameters") + private List parameters; + + /** + * @return the exceptionClass + */ + public String getExceptionClass() { + return exceptionClass; + } + + /** + * @param exceptionClass + * the exceptionClass to set + */ + public void setExceptionClass(final String exceptionClass) { + this.exceptionClass = exceptionClass; + } + + /** + * @return the parameters + */ + public List getParameters() { + return parameters; + } + + /** + * @param parameters + * the parameters to set + */ + public void setParameters(final List parameters) { + this.parameters = parameters; + } + + /** + * @return the errorCode + */ + public String getErrorCode() { + return errorCode; + } + + /** + * @param errorCode + * the errorCode to set + */ + public void setErrorCode(final String errorCode) { + this.errorCode = errorCode; + } + + /** + * @return the message + */ + public String getMessage() { + return message; + } + + /** + * @param message + * the message to set + */ + public void setMessage(final String message) { + this.message = message; + } +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/IdRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/IdRest.java new file mode 100755 index 000000000..efc33feb1 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/IdRest.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModelProperty; + +/** + * A generic abstract rest model which contains only a ID for use-case e.g. + * which allows only posting or putting an ID into the request body, e.g. for + * assignments. + * + * + * + * + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class IdRest { + + @ApiModelProperty(value = ApiModelProperties.ITEM_ID) + @JsonProperty + private Long id; + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id + * the id to set + */ + public void setId(final Long id) { + this.id = id; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/MetadataRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/MetadataRest.java new file mode 100755 index 000000000..4ddabd1aa --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/MetadataRest.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * The representation of an meta data in the REST API. + * + * + * + * + */ +@ApiModel(ApiModelProperties.META_DATA) +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class MetadataRest { + + @ApiModelProperty(required = true, value = "The key of the meta data") + @JsonProperty(required = true) + private String key; + @ApiModelProperty(required = false, value = "The value of the meta data") + @JsonProperty + private String value; + + /** + * @return the key + */ + public String getKey() { + return key; + } + + /** + * @param key + * the key to set + */ + public void setKey(final String key) { + this.key = key; + } + + /** + * @return the value + */ + public String getValue() { + return value; + } + + /** + * @param value + * the value to set + */ + public void setValue(final String value) { + this.value = value; + } +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/MetadataRestPageList.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/MetadataRestPageList.java new file mode 100755 index 000000000..3d363ad81 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/MetadataRestPageList.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model; + +import java.util.List; + +import io.swagger.annotations.ApiModelProperty; + +/** + * The rest model for a paged meta data list. + * + * + * + * + */ +public class MetadataRestPageList extends PagedList { + + @ApiModelProperty(value = "a list of meta data", required = true) + private final List content; + + /** + * @param content + * the meta data rest model list as content + * @param total + * the total number of the meta data + */ + public MetadataRestPageList(final List content, final long total) { + super(content, total); + this.content = content; + } + + /** + * @return the content of this paged list + */ + public List getContent() { + return content; + } +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/NamedEntityRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/NamedEntityRest.java new file mode 100755 index 000000000..fb02ba1f2 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/NamedEntityRest.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModelProperty; + +/** + * A json annotated rest model for NamedEntity to RESTful API representation. + * + * + * + * + */ +public abstract class NamedEntityRest extends BaseEntityRest { + + @ApiModelProperty(value = ApiModelProperties.NAME, required = true) + @JsonProperty(required = true) + private String name; + + @ApiModelProperty(value = ApiModelProperties.DESCRPTION) + @JsonProperty + private String description; + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name + * the name to set + */ + public void setName(final String name) { + this.name = name; + } + + /** + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * @param description + * the description to set + */ + public void setDescription(final String description) { + this.description = description; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/PagedList.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/PagedList.java new file mode 100755 index 000000000..3356d3a31 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/PagedList.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model; + +import java.util.List; + +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; +import org.springframework.hateoas.ResourceSupport; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +import io.swagger.annotations.ApiModelProperty; + +/** + * A list representation with meta data for pagination, e.g. containing the + * total elements. The content of the acutal list is stored in the + * {@link #content} field. + * + * @param + * the type of elements in this list + * + * + * + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(Include.NON_NULL) +public class PagedList extends ResourceSupport { + + @ApiModelProperty(value = ApiModelProperties.TOTAL_ELEMENTS, required = false) + private final long totalElements; + + @ApiModelProperty(value = ApiModelProperties.SIZE, required = true) + private final int size; + + /** + * creates a new paged list with the given {@code content} and {@code total} + * . + * + * @param content + * the actual content of the list + * @param total + * the total amount of elements + * @throws NullPointerException + * in case {@code content} is {@code null}. + */ + public PagedList(@NotNull final List content, final long total) { + this.size = content.size(); + this.totalElements = total; + } + + /** + * @return the size of the content list + */ + public int getSize() { + return size; + } + + /** + * @return the total amount of elements + */ + public long getTotal() { + return totalElements; + } +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/PollStatusRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/PollStatusRest.java new file mode 100755 index 000000000..c64cf708f --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/PollStatusRest.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * A json annotated rest model for PollStatus to RESTful API representation. + * + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@ApiModel("Target Poll Status") +public class PollStatusRest { + + @ApiModelProperty(value = ApiModelProperties.POLL_LAST_REQUEST_AT) + @JsonProperty + private Long lastRequestAt; + + @ApiModelProperty(value = ApiModelProperties.POLL_NEXT_EXPECTED_REQUEST_AT) + @JsonProperty + private Long nextExpectedRequestAt; + + @ApiModelProperty(value = ApiModelProperties.POLL_OVERDUE) + @JsonProperty + private boolean overdue; + + /** + * @return the lastRequestAt + */ + public Long getLastRequestAt() { + return lastRequestAt; + } + + /** + * @param lastRequestAt + * the lastRequestAt to set + */ + public void setLastRequestAt(final Long lastRequestAt) { + this.lastRequestAt = lastRequestAt; + } + + /** + * @return the nextExpectedRequestAt + */ + public Long getNextExpectedRequestAt() { + return nextExpectedRequestAt; + } + + /** + * @param nextExpectedRequestAt + * the nextExpectedRequestAt to set + */ + public void setNextExpectedRequestAt(final Long nextExpectedRequestAt) { + this.nextExpectedRequestAt = nextExpectedRequestAt; + } + + /** + * @return the overdue + */ + public boolean isOverdue() { + return overdue; + } + + /** + * @param overdue + * the overdue to set + */ + public void setOverdue(final boolean overdue) { + this.overdue = overdue; + } +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionPagedList.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionPagedList.java new file mode 100755 index 000000000..aedd2938f --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionPagedList.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.action; + +import java.util.Collections; +import java.util.List; + +import org.eclipse.hawkbit.rest.resource.model.PagedList; +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Paged list rest model for {@link Action} to RESTful API representation. + * + * + * + * + */ +@ApiModel("Paged list of actions") +public class ActionPagedList extends PagedList { + + @ApiModelProperty(value = ApiModelProperties.ACTION_LIST) + private final List content; + + /** + * Empty default constructor. + */ + public ActionPagedList() { + super(Collections.emptyList(), 0); + this.content = Collections.emptyList(); + } + + /** + * @param content + * @param total + */ + public ActionPagedList(final List content, final long total) { + super(content, total); + this.content = content; + } + + /** + * @return the content of the paged list. Never {@code null}. + */ + public List getContent() { + return content; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionRest.java new file mode 100755 index 000000000..e8b5bd299 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionRest.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.action; + +import org.eclipse.hawkbit.rest.resource.model.BaseEntityRest; +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * A json annotated rest model for Action to RESTful API representation. + * + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@ApiModel("SP Action") +public class ActionRest extends BaseEntityRest { + + /** + * API definition for {@link UpdateAction}. + */ + public static final String ACTION_UPDATE = "update"; + + /** + * API definition for {@link CancelAction}. + */ + public static final String ACTION_CANCEL = "cancel"; + + public static final String ACTION_FINISHED = "finished"; + + public static final String ACTION_PENDING = "pending"; + + @ApiModelProperty(value = ApiModelProperties.ITEM_ID) + @JsonProperty("id") + private Long actionId; + + @ApiModelProperty(value = ApiModelProperties.ACTION_TYPE, allowableValues = ACTION_UPDATE + "," + ACTION_CANCEL) + @JsonProperty + private String type; + + @ApiModelProperty(value = ApiModelProperties.ACTION_EXECUTION_STATUS, allowableValues = ACTION_FINISHED + "," + + ACTION_PENDING) + @JsonProperty + private String status; + + /** + * @return the status + */ + public String getStatus() { + return status; + } + + /** + * @param status + * the status to set + */ + public void setStatus(final String status) { + this.status = status; + } + + /** + * @return the actionId + */ + public Long getActionId() { + return actionId; + } + + /** + * @param actionId + * the actionId to set + */ + public void setActionId(final Long actionId) { + this.actionId = actionId; + } + + /** + * @return the type + */ + public String getType() { + return type; + } + + /** + * @param type + * the type to set + */ + public void setType(final String type) { + this.type = type; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionStatusPagedList.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionStatusPagedList.java new file mode 100755 index 000000000..2595cd536 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionStatusPagedList.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.action; + +import java.util.Collections; +import java.util.List; + +import org.eclipse.hawkbit.rest.resource.model.PagedList; +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Paged list rest model for ActionStatus to RESTful API representation. + * + * + * + * + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@ApiModel("Paged list of action status") +public class ActionStatusPagedList extends PagedList { + + @ApiModelProperty(value = ApiModelProperties.ACTION_STATUS_LIST) + private final List content; + + /** + * @param content + * @param total + */ + public ActionStatusPagedList(final List content, final long total) { + super(content, total); + this.content = content; + } + + /** + * Default constructor. + */ + public ActionStatusPagedList() { + super(Collections.emptyList(), 0); + this.content = Collections.emptyList(); + } + + /** + * @return the content of the paged list. Never {@code null}. + */ + public List getContent() { + return content; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionStatusRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionStatusRest.java new file mode 100755 index 000000000..b66cb666d --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionStatusRest.java @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.action; + +import java.util.List; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * A json annotated rest model for ActionStatus to RESTful API representation. + * + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@ApiModel("SP Action Status") +public class ActionStatusRest { + + /** + * Action is finished successfully for this target. + */ + public static final String AS_FINISHED = "finished"; + + /** + * Action has failed for this target. + */ + public static final String AS_ERROR = "error"; + + /** + * Action is still running but with warnings. + */ + public static final String AS_WARNING = "warning"; + + /** + * Action is still running for this target. + */ + public static final String AS_RUNNING = "running"; + /** + * Action has been canceled for this target. + */ + public static final String AS_CANCELED = "canceled"; + + /** + * Action has been presented to the target. + */ + public static final String AS_RETRIEVED = "retrieved"; + + @ApiModelProperty(value = ApiModelProperties.ITEM_ID) + @JsonProperty("id") + private Long statusId; + + @ApiModelProperty(value = ApiModelProperties.ACTION_STATUS_TYPE, required = true, allowableValues = AS_FINISHED + + "," + AS_ERROR + "," + AS_WARNING + "," + AS_RUNNING + "," + AS_CANCELED + "," + AS_RETRIEVED) + @JsonProperty + private String type; + + @ApiModelProperty(value = ApiModelProperties.ACTION_STATUS_MESSAGES) + @JsonProperty + private List messages; + + @ApiModelProperty(value = ApiModelProperties.ACTION_STATUS_REPORTED_AT) + @JsonProperty + private Long reportedAt; + + /** + * @return the statusId + */ + public Long getStatusId() { + return statusId; + } + + /** + * @param statusId + * the statusId to set + */ + public void setStatusId(final Long statusId) { + this.statusId = statusId; + } + + /** + * @return the type + */ + public String getType() { + return type; + } + + /** + * @param type + * the type to set + */ + public void setType(final String type) { + this.type = type; + } + + /** + * @return the messages + */ + public List getMessages() { + return messages; + } + + /** + * @param messages + * the messages to set + */ + public void setMessages(final List messages) { + this.messages = messages; + } + + /** + * @return the reportedAt + */ + public Long getReportedAt() { + return reportedAt; + } + + /** + * @param reportedAt + * the reportedAt to set + */ + public void setReportedAt(final Long reportedAt) { + this.reportedAt = reportedAt; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionStatussRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionStatussRest.java new file mode 100755 index 000000000..b3f3207b4 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionStatussRest.java @@ -0,0 +1,277 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.action; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import org.springframework.hateoas.ResourceSupport; + +/** + * A list representation of the ActionStatus because Spring MVC cannot handle + * plain lists interfaces as request body. + * + * + * + * + */ +public class ActionStatussRest extends ResourceSupport implements List { + + private final List delegate = new ArrayList<>(); + + /** + * @return + * @see java.util.List#size() + */ + @Override + public int size() { + return delegate.size(); + } + + /** + * @return + * @see java.util.List#isEmpty() + */ + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + /** + * @param o + * @return + * @see java.util.List#contains(java.lang.Object) + */ + @Override + public boolean contains(final Object o) { + return delegate.contains(o); + } + + /** + * @return + * @see java.util.List#iterator() + */ + @Override + public Iterator iterator() { + return delegate.iterator(); + } + + /** + * @return + * @see java.util.List#toArray() + */ + @Override + public Object[] toArray() { + return delegate.toArray(); + } + + /** + * @param a + * @return + * @see java.util.List#toArray(java.lang.Object[]) + */ + @Override + public T[] toArray(final T[] a) { + return delegate.toArray(a); + } + + /** + * @param e + * @return + * @see java.util.List#add(java.lang.Object) + */ + @Override + public boolean add(final ActionStatusRest e) { + return delegate.add(e); + } + + /** + * @param o + * @return + * @see java.util.List#remove(java.lang.Object) + */ + @Override + public boolean remove(final Object o) { + return delegate.remove(o); + } + + /** + * @param c + * @return + * @see java.util.List#containsAll(java.util.Collection) + */ + @Override + public boolean containsAll(final Collection c) { + return delegate.containsAll(c); + } + + /** + * @param c + * @return + * @see java.util.List#addAll(java.util.Collection) + */ + @Override + public boolean addAll(final Collection c) { + return delegate.addAll(c); + } + + /** + * @param index + * @param c + * @return + * @see java.util.List#addAll(int, java.util.Collection) + */ + @Override + public boolean addAll(final int index, final Collection c) { + return delegate.addAll(index, c); + } + + /** + * @param c + * @return + * @see java.util.List#removeAll(java.util.Collection) + */ + @Override + public boolean removeAll(final Collection c) { + return delegate.removeAll(c); + } + + /** + * @param c + * @return + * @see java.util.List#retainAll(java.util.Collection) + */ + @Override + public boolean retainAll(final Collection c) { + return delegate.retainAll(c); + } + + /** + * + * @see java.util.List#clear() + */ + @Override + public void clear() { + delegate.clear(); + } + + /** + * @param o + * @return + * @see java.util.List#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object o) { + return delegate.equals(o); + } + + /** + * @return + * @see java.util.List#hashCode() + */ + @Override + public int hashCode() { + return delegate.hashCode(); + } + + /** + * @param index + * @return + * @see java.util.List#get(int) + */ + @Override + public ActionStatusRest get(final int index) { + return delegate.get(index); + } + + /** + * @param index + * @param element + * @return + * @see java.util.List#set(int, java.lang.Object) + */ + @Override + public ActionStatusRest set(final int index, final ActionStatusRest element) { + return delegate.set(index, element); + } + + /** + * @param index + * @param element + * @see java.util.List#add(int, java.lang.Object) + */ + @Override + public void add(final int index, final ActionStatusRest element) { + delegate.add(index, element); + } + + /** + * @param index + * @return + * @see java.util.List#remove(int) + */ + @Override + public ActionStatusRest remove(final int index) { + return delegate.remove(index); + } + + /** + * @param o + * @return + * @see java.util.List#indexOf(java.lang.Object) + */ + @Override + public int indexOf(final Object o) { + return delegate.indexOf(o); + } + + /** + * @param o + * @return + * @see java.util.List#lastIndexOf(java.lang.Object) + */ + @Override + public int lastIndexOf(final Object o) { + return delegate.lastIndexOf(o); + } + + /** + * @return + * @see java.util.List#listIterator() + */ + @Override + public ListIterator listIterator() { + return delegate.listIterator(); + } + + /** + * @param index + * @return + * @see java.util.List#listIterator(int) + */ + @Override + public ListIterator listIterator(final int index) { + return delegate.listIterator(index); + } + + /** + * @param fromIndex + * @param toIndex + * @return + * @see java.util.List#subList(int, int) + */ + @Override + public List subList(final int fromIndex, final int toIndex) { + return delegate.subList(fromIndex, toIndex); + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionsRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionsRest.java new file mode 100755 index 000000000..9c3659f30 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/action/ActionsRest.java @@ -0,0 +1,276 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.action; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import org.springframework.hateoas.ResourceSupport; + +/** + * A json annotated rest model for Actions to RESTful API representation. + * + * + * + * + */ +public class ActionsRest extends ResourceSupport implements List { + + private final List delegate = new ArrayList<>(); + + /** + * @return + * @see java.util.List#size() + */ + @Override + public int size() { + return delegate.size(); + } + + /** + * @return + * @see java.util.List#isEmpty() + */ + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + /** + * @param o + * @return + * @see java.util.List#contains(java.lang.Object) + */ + @Override + public boolean contains(final Object o) { + return delegate.contains(o); + } + + /** + * @return + * @see java.util.List#iterator() + */ + @Override + public Iterator iterator() { + return delegate.iterator(); + } + + /** + * @return + * @see java.util.List#toArray() + */ + @Override + public Object[] toArray() { + return delegate.toArray(); + } + + /** + * @param a + * @return + * @see java.util.List#toArray(java.lang.Object[]) + */ + @Override + public T[] toArray(final T[] a) { + return delegate.toArray(a); + } + + /** + * @param e + * @return + * @see java.util.List#add(java.lang.Object) + */ + @Override + public boolean add(final ActionRest e) { + return delegate.add(e); + } + + /** + * @param o + * @return + * @see java.util.List#remove(java.lang.Object) + */ + @Override + public boolean remove(final Object o) { + return delegate.remove(o); + } + + /** + * @param c + * @return + * @see java.util.List#containsAll(java.util.Collection) + */ + @Override + public boolean containsAll(final Collection c) { + return delegate.containsAll(c); + } + + /** + * @param c + * @return + * @see java.util.List#addAll(java.util.Collection) + */ + @Override + public boolean addAll(final Collection c) { + return delegate.addAll(c); + } + + /** + * @param index + * @param c + * @return + * @see java.util.List#addAll(int, java.util.Collection) + */ + @Override + public boolean addAll(final int index, final Collection c) { + return delegate.addAll(index, c); + } + + /** + * @param c + * @return + * @see java.util.List#removeAll(java.util.Collection) + */ + @Override + public boolean removeAll(final Collection c) { + return delegate.removeAll(c); + } + + /** + * @param c + * @return + * @see java.util.List#retainAll(java.util.Collection) + */ + @Override + public boolean retainAll(final Collection c) { + return delegate.retainAll(c); + } + + /** + * + * @see java.util.List#clear() + */ + @Override + public void clear() { + delegate.clear(); + } + + /** + * @param o + * @return + * @see java.util.List#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object o) { + return delegate.equals(o); + } + + /** + * @return + * @see java.util.List#hashCode() + */ + @Override + public int hashCode() { + return delegate.hashCode(); + } + + /** + * @param index + * @return + * @see java.util.List#get(int) + */ + @Override + public ActionRest get(final int index) { + return delegate.get(index); + } + + /** + * @param index + * @param element + * @return + * @see java.util.List#set(int, java.lang.Object) + */ + @Override + public ActionRest set(final int index, final ActionRest element) { + return delegate.set(index, element); + } + + /** + * @param index + * @param element + * @see java.util.List#add(int, java.lang.Object) + */ + @Override + public void add(final int index, final ActionRest element) { + delegate.add(index, element); + } + + /** + * @param index + * @return + * @see java.util.List#remove(int) + */ + @Override + public ActionRest remove(final int index) { + return delegate.remove(index); + } + + /** + * @param o + * @return + * @see java.util.List#indexOf(java.lang.Object) + */ + @Override + public int indexOf(final Object o) { + return delegate.indexOf(o); + } + + /** + * @param o + * @return + * @see java.util.List#lastIndexOf(java.lang.Object) + */ + @Override + public int lastIndexOf(final Object o) { + return delegate.lastIndexOf(o); + } + + /** + * @return + * @see java.util.List#listIterator() + */ + @Override + public ListIterator listIterator() { + return delegate.listIterator(); + } + + /** + * @param index + * @return + * @see java.util.List#listIterator(int) + */ + @Override + public ListIterator listIterator(final int index) { + return delegate.listIterator(index); + } + + /** + * @param fromIndex + * @param toIndex + * @return + * @see java.util.List#subList(int, int) + */ + @Override + public List subList(final int fromIndex, final int toIndex) { + return delegate.subList(fromIndex, toIndex); + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/artifact/ArtifactHash.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/artifact/ArtifactHash.java new file mode 100755 index 000000000..5d203b8c2 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/artifact/ArtifactHash.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.artifact; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Hashes for given Artifact. + * + * + * + * + */ +@ApiModel(ApiModelProperties.ARTIFACT_HASHES) +public class ArtifactHash { + + @ApiModelProperty(value = ApiModelProperties.ARTIFACT_HASHES_SHA1) + @JsonProperty + private String sha1; + + @ApiModelProperty(value = ApiModelProperties.ARTIFACT_HASHES_MD5) + @JsonProperty + private String md5; + + /** + * Default constructor. + */ + public ArtifactHash() { + } + + /** + * Public constructor. + * + * @param sha1 + * @param md5 + */ + public ArtifactHash(final String sha1, final String md5) { + super(); + this.sha1 = sha1; + this.md5 = md5; + } + + /** + * @return the sha1 + */ + public String getSha1() { + return sha1; + } + + /** + * @return the md5 + */ + public String getMd5() { + return md5; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/artifact/ArtifactRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/artifact/ArtifactRest.java new file mode 100755 index 000000000..adf614aa8 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/artifact/ArtifactRest.java @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.artifact; + +import org.eclipse.hawkbit.rest.resource.model.BaseEntityRest; +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * A json annotated rest model for Artifact to RESTful API representation. + * + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@ApiModel("Software Artifact") +public class ArtifactRest extends BaseEntityRest { + @JsonProperty(required = true) + private ArtifactType type; + + @ApiModelProperty(value = ApiModelProperties.ITEM_ID, required = true) + @JsonProperty("id") + private Long artifactId; + + @ApiModelProperty(value = ApiModelProperties.ARTIFACT_HASHES) + @JsonProperty + private ArtifactHash hashes; + + @ApiModelProperty(value = ApiModelProperties.ARTIFACT_PROVIDED_FILENAME) + @JsonProperty + private String providedFilename; + + @ApiModelProperty(value = ApiModelProperties.ARTIFACT_SIZE) + @JsonProperty + private Long size; + + /** + * @param type + * the type to set + */ + public void setType(final ArtifactType type) { + this.type = type; + } + + /** + * @param hashes + * the hashes to set + */ + @JsonIgnore + public void setHashes(final ArtifactHash hashes) { + this.hashes = hashes; + } + + /** + * @param artifactId + * the artifactId to set + */ + @JsonIgnore + public void setArtifactId(final Long artifactId) { + this.artifactId = artifactId; + } + + /** + * @return the type + */ + public ArtifactType getType() { + return type; + } + + /** + * @return the artifactId + */ + public Long getArtifactId() { + return artifactId; + } + + /** + * @return the hashes + */ + public ArtifactHash getHashes() { + return hashes; + } + + /** + * @return the providedFilename + */ + public String getProvidedFilename() { + return providedFilename; + } + + /** + * @param providedFilename + * the providedFilename to set + */ + @JsonIgnore + public void setProvidedFilename(final String providedFilename) { + this.providedFilename = providedFilename; + } + + /** + * Type maps to either {@link LocalArtifact} or {@link ExternalArtifact}. + * + * + * + * + */ + public enum ArtifactType { + LOCAL("local"), EXTERNAL("external"); + + private final String name; + + private ArtifactType(final String name) { + this.name = name; + } + + /** + * @return the name + */ + @JsonValue + public String getName() { + return name; + } + } + + /** + * @return the size + */ + public Long getSize() { + return size; + } + + /** + * @param size + * the size to set + */ + @JsonIgnore + public void setSize(final Long size) { + this.size = size; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/artifact/ArtifactsRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/artifact/ArtifactsRest.java new file mode 100755 index 000000000..32f4ca531 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/artifact/ArtifactsRest.java @@ -0,0 +1,279 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.artifact; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import org.springframework.hateoas.ResourceSupport; + +import io.swagger.annotations.ApiModel; + +/** + * List representation of the {@link ArtifactRest}. + * + * + * + * + */ +@ApiModel +public class ArtifactsRest extends ResourceSupport implements List { + + private final List delegate = new ArrayList<>(); + + /** + * @return + * @see java.util.List#size() + */ + @Override + public int size() { + return delegate.size(); + } + + /** + * @return + * @see java.util.List#isEmpty() + */ + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + /** + * @param o + * @return + * @see java.util.List#contains(java.lang.Object) + */ + @Override + public boolean contains(final Object o) { + return delegate.contains(o); + } + + /** + * @return + * @see java.util.List#iterator() + */ + @Override + public Iterator iterator() { + return delegate.iterator(); + } + + /** + * @return + * @see java.util.List#toArray() + */ + @Override + public Object[] toArray() { + return delegate.toArray(); + } + + /** + * @param a + * @return + * @see java.util.List#toArray(java.lang.Object[]) + */ + @Override + public T[] toArray(final T[] a) { + return delegate.toArray(a); + } + + /** + * @param e + * @return + * @see java.util.List#add(java.lang.Object) + */ + @Override + public boolean add(final ArtifactRest e) { + return delegate.add(e); + } + + /** + * @param o + * @return + * @see java.util.List#remove(java.lang.Object) + */ + @Override + public boolean remove(final Object o) { + return delegate.remove(o); + } + + /** + * @param c + * @return + * @see java.util.List#containsAll(java.util.Collection) + */ + @Override + public boolean containsAll(final Collection c) { + return delegate.containsAll(c); + } + + /** + * @param c + * @return + * @see java.util.List#addAll(java.util.Collection) + */ + @Override + public boolean addAll(final Collection c) { + return delegate.addAll(c); + } + + /** + * @param index + * @param c + * @return + * @see java.util.List#addAll(int, java.util.Collection) + */ + @Override + public boolean addAll(final int index, final Collection c) { + return delegate.addAll(index, c); + } + + /** + * @param c + * @return + * @see java.util.List#removeAll(java.util.Collection) + */ + @Override + public boolean removeAll(final Collection c) { + return delegate.removeAll(c); + } + + /** + * @param c + * @return + * @see java.util.List#retainAll(java.util.Collection) + */ + @Override + public boolean retainAll(final Collection c) { + return delegate.retainAll(c); + } + + /** + * + * @see java.util.List#clear() + */ + @Override + public void clear() { + delegate.clear(); + } + + /** + * @param o + * @return + * @see java.util.List#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object o) { + return delegate.equals(o); + } + + /** + * @return + * @see java.util.List#hashCode() + */ + @Override + public int hashCode() { + return delegate.hashCode(); + } + + /** + * @param index + * @return + * @see java.util.List#get(int) + */ + @Override + public ArtifactRest get(final int index) { + return delegate.get(index); + } + + /** + * @param index + * @param element + * @return + * @see java.util.List#set(int, java.lang.Object) + */ + @Override + public ArtifactRest set(final int index, final ArtifactRest element) { + return delegate.set(index, element); + } + + /** + * @param index + * @param element + * @see java.util.List#add(int, java.lang.Object) + */ + @Override + public void add(final int index, final ArtifactRest element) { + delegate.add(index, element); + } + + /** + * @param index + * @return + * @see java.util.List#remove(int) + */ + @Override + public ArtifactRest remove(final int index) { + return delegate.remove(index); + } + + /** + * @param o + * @return + * @see java.util.List#indexOf(java.lang.Object) + */ + @Override + public int indexOf(final Object o) { + return delegate.indexOf(o); + } + + /** + * @param o + * @return + * @see java.util.List#lastIndexOf(java.lang.Object) + */ + @Override + public int lastIndexOf(final Object o) { + return delegate.lastIndexOf(o); + } + + /** + * @return + * @see java.util.List#listIterator() + */ + @Override + public ListIterator listIterator() { + return delegate.listIterator(); + } + + /** + * @param index + * @return + * @see java.util.List#listIterator(int) + */ + @Override + public ListIterator listIterator(final int index) { + return delegate.listIterator(index); + } + + /** + * @param fromIndex + * @param toIndex + * @return + * @see java.util.List#subList(int, int) + */ + @Override + public List subList(final int fromIndex, final int toIndex) { + return delegate.subList(fromIndex, toIndex); + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/ActionTypeRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/ActionTypeRest.java new file mode 100755 index 000000000..bf6a649a0 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/ActionTypeRest.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.distributionset; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Definition of the {@link ActionType} for the REST management API. + * + * + * + * + */ +public enum ActionTypeRest { + /** + * The soft action type. + */ + SOFT("soft"), + + /** + * The forced action type. + */ + FORCED("forced"), + + /** + * The time forced action type. + */ + TIMEFORCED("timeforced"); + + private final String name; + + private ActionTypeRest(final String name) { + this.name = name; + } + + /** + * @return the name + */ + @JsonValue + public String getName() { + return name; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/DistributionSetPagedList.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/DistributionSetPagedList.java new file mode 100755 index 000000000..b631e4500 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/DistributionSetPagedList.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.distributionset; + +import java.util.List; + +import org.eclipse.hawkbit.rest.resource.model.PagedList; +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Paged list for SoftwareModule. + * + * + * + * + */ +@ApiModel("Paged list of distribution sets") +public class DistributionSetPagedList extends PagedList { + + @ApiModelProperty(value = ApiModelProperties.SM_LIST, required = true) + private final List content; + + /** + * @param content + * @param total + */ + public DistributionSetPagedList(final List content, final long total) { + super(content, total); + this.content = content; + } + + /** + * @return the content of the paged list. Never {@code null}. + */ + public List getContent() { + return content; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/DistributionSetRequestBodyCreate.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/DistributionSetRequestBodyCreate.java new file mode 100755 index 000000000..896a3275b --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/DistributionSetRequestBodyCreate.java @@ -0,0 +1,153 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.distributionset; + +import java.util.List; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; +import org.eclipse.hawkbit.rest.resource.model.softwaremodule.SoftwareModuleAssigmentRest; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * A json annotated rest model for DistributionSet for POST. + * + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@ApiModel("Distribution set") +public class DistributionSetRequestBodyCreate extends DistributionSetRequestBodyUpdate { + + // deprecated format from the time where os, application and runtime where + // statically defined + @ApiModelProperty(hidden = true) + @JsonProperty + private SoftwareModuleAssigmentRest os; + + @ApiModelProperty(hidden = true) + @JsonProperty + private SoftwareModuleAssigmentRest runtime; + + @ApiModelProperty(hidden = true) + @JsonProperty + private SoftwareModuleAssigmentRest application; + // deprecated format - END + + @ApiModelProperty(value = ApiModelProperties.DS_MODULES) + @JsonProperty + private List modules; + + @ApiModelProperty(value = ApiModelProperties.DS_REQUIRED_STEP) + @JsonProperty + private boolean requiredMigrationStep; + + @ApiModelProperty(value = ApiModelProperties.DS_TYPE) + @JsonProperty + private String type; + + /** + * @return the os + */ + public SoftwareModuleAssigmentRest getOs() { + return os; + } + + /** + * @param os + * the os to set + */ + public void setOs(final SoftwareModuleAssigmentRest os) { + this.os = os; + } + + /** + * @return the runtime + */ + public SoftwareModuleAssigmentRest getRuntime() { + return runtime; + } + + /** + * @param runtime + * the runtime to set + */ + public void setRuntime(final SoftwareModuleAssigmentRest runtime) { + this.runtime = runtime; + } + + /** + * @return the application + */ + public SoftwareModuleAssigmentRest getApplication() { + return application; + } + + /** + * @param application + * the application to set + */ + public void setApplication(final SoftwareModuleAssigmentRest application) { + this.application = application; + } + + /** + * @return the requiredMigrationStep + */ + public boolean isRequiredMigrationStep() { + return requiredMigrationStep; + } + + /** + * @param requiredMigrationStep + * the requiredMigrationStep to set + */ + public void setRequiredMigrationStep(final boolean requiredMigrationStep) { + this.requiredMigrationStep = requiredMigrationStep; + } + + /** + * @return the modules + */ + public List getModules() { + return modules; + } + + /** + * @param modules + * the modules to set + */ + public void setModules(final List modules) { + this.modules = modules; + } + + /** + * @return the type + */ + public String getType() { + return type; + } + + /** + * @param type + * the type to set + */ + public void setType(final String type) { + this.type = type; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/DistributionSetRequestBodyUpdate.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/DistributionSetRequestBodyUpdate.java new file mode 100755 index 000000000..e93533a52 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/DistributionSetRequestBodyUpdate.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.distributionset; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * A json annotated rest model for DistributionSet for PUT/POST. + * + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@ApiModel("Distribution set") +public class DistributionSetRequestBodyUpdate { + + @ApiModelProperty(value = ApiModelProperties.NAME, required = true) + @JsonProperty(required = true) + private String name; + + @ApiModelProperty(value = ApiModelProperties.DESCRPTION) + @JsonProperty + private String description; + + @ApiModelProperty(value = ApiModelProperties.VERSION) + @JsonProperty + private String version; + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name + * the name to set + */ + public void setName(final String name) { + this.name = name; + } + + /** + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * @param description + * the description to set + */ + public void setDescription(final String description) { + this.description = description; + } + + /** + * @return the version + */ + public String getVersion() { + return version; + } + + /** + * @param version + * the version to set + */ + public void setVersion(final String version) { + this.version = version; + } +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/DistributionSetRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/DistributionSetRest.java new file mode 100755 index 000000000..6c5e432df --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/DistributionSetRest.java @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.distributionset; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.hawkbit.rest.resource.model.NamedEntityRest; +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; +import org.eclipse.hawkbit.rest.resource.model.softwaremodule.SoftwareModuleRest; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * A json annotated rest model for DistributionSet to RESTful API + * representation. + * + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@ApiModel("Distribution set") +public class DistributionSetRest extends NamedEntityRest { + + @ApiModelProperty(value = ApiModelProperties.ITEM_ID) + @JsonProperty(value = "id", required = true) + private Long dsId; + + @ApiModelProperty(value = ApiModelProperties.VERSION) + @JsonProperty + private String version; + + @ApiModelProperty(value = ApiModelProperties.DS_MODULES) + @JsonProperty + private List modules = new ArrayList(); + + @ApiModelProperty(value = ApiModelProperties.DS_REQUIRED_STEP) + @JsonProperty + private boolean requiredMigrationStep; + + @ApiModelProperty(value = ApiModelProperties.DS_TYPE) + @JsonProperty + private String type; + + @ApiModelProperty(value = ApiModelProperties.DS_COMPLETE) + @JsonProperty + private Boolean complete; + + /** + * @return the id + */ + public Long getDsId() { + return dsId; + } + + /** + * @param id + * the id to set + */ + @JsonIgnore + public void setDsId(final Long id) { + dsId = id; + } + + /** + * @return the version + */ + public String getVersion() { + return version; + } + + /** + * @param version + * the version to set + */ + public void setVersion(final String version) { + this.version = version; + } + + /** + * @return the requiredMigrationStep + */ + public boolean isRequiredMigrationStep() { + return requiredMigrationStep; + } + + /** + * @param requiredMigrationStep + * the requiredMigrationStep to set + */ + public void setRequiredMigrationStep(final boolean requiredMigrationStep) { + this.requiredMigrationStep = requiredMigrationStep; + } + + /** + * @return the modules + */ + public List getModules() { + return modules; + } + + /** + * @param modules + * the modules to set + */ + public void setModules(final List modules) { + this.modules = modules; + } + + /** + * @return the type + */ + public String getType() { + return type; + } + + /** + * @param type + * the type to set + */ + public void setType(final String type) { + this.type = type; + } + + /** + * @return the complete + */ + public Boolean getComplete() { + return complete; + } + + /** + * @param complete + * the complete to set + */ + public void setComplete(final Boolean complete) { + this.complete = complete; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/DistributionSetsRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/DistributionSetsRest.java new file mode 100755 index 000000000..ce7b4d1f1 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/DistributionSetsRest.java @@ -0,0 +1,277 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.distributionset; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import org.springframework.hateoas.ResourceSupport; + +/** + * List representation of the {@link DistributionSetRest} because Spring MVC + * cannot handle plain lists interfaces as request body. + * + * + * + * + */ +public class DistributionSetsRest extends ResourceSupport implements List { + + private final List delegate = new ArrayList<>(); + + /** + * @return + * @see java.util.List#size() + */ + @Override + public int size() { + return delegate.size(); + } + + /** + * @return + * @see java.util.List#isEmpty() + */ + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + /** + * @param o + * @return + * @see java.util.List#contains(java.lang.Object) + */ + @Override + public boolean contains(final Object o) { + return delegate.contains(o); + } + + /** + * @return + * @see java.util.List#iterator() + */ + @Override + public Iterator iterator() { + return delegate.iterator(); + } + + /** + * @return + * @see java.util.List#toArray() + */ + @Override + public Object[] toArray() { + return delegate.toArray(); + } + + /** + * @param a + * @return + * @see java.util.List#toArray(java.lang.Object[]) + */ + @Override + public T[] toArray(final T[] a) { + return delegate.toArray(a); + } + + /** + * @param e + * @return + * @see java.util.List#add(java.lang.Object) + */ + @Override + public boolean add(final DistributionSetRest e) { + return delegate.add(e); + } + + /** + * @param o + * @return + * @see java.util.List#remove(java.lang.Object) + */ + @Override + public boolean remove(final Object o) { + return delegate.remove(o); + } + + /** + * @param c + * @return + * @see java.util.List#containsAll(java.util.Collection) + */ + @Override + public boolean containsAll(final Collection c) { + return delegate.containsAll(c); + } + + /** + * @param c + * @return + * @see java.util.List#addAll(java.util.Collection) + */ + @Override + public boolean addAll(final Collection c) { + return delegate.addAll(c); + } + + /** + * @param index + * @param c + * @return + * @see java.util.List#addAll(int, java.util.Collection) + */ + @Override + public boolean addAll(final int index, final Collection c) { + return delegate.addAll(index, c); + } + + /** + * @param c + * @return + * @see java.util.List#removeAll(java.util.Collection) + */ + @Override + public boolean removeAll(final Collection c) { + return delegate.removeAll(c); + } + + /** + * @param c + * @return + * @see java.util.List#retainAll(java.util.Collection) + */ + @Override + public boolean retainAll(final Collection c) { + return delegate.retainAll(c); + } + + /** + * + * @see java.util.List#clear() + */ + @Override + public void clear() { + delegate.clear(); + } + + /** + * @param o + * @return + * @see java.util.List#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object o) { + return delegate.equals(o); + } + + /** + * @return + * @see java.util.List#hashCode() + */ + @Override + public int hashCode() { + return delegate.hashCode(); + } + + /** + * @param index + * @return + * @see java.util.List#get(int) + */ + @Override + public DistributionSetRest get(final int index) { + return delegate.get(index); + } + + /** + * @param index + * @param element + * @return + * @see java.util.List#set(int, java.lang.Object) + */ + @Override + public DistributionSetRest set(final int index, final DistributionSetRest element) { + return delegate.set(index, element); + } + + /** + * @param index + * @param element + * @see java.util.List#add(int, java.lang.Object) + */ + @Override + public void add(final int index, final DistributionSetRest element) { + delegate.add(index, element); + } + + /** + * @param index + * @return + * @see java.util.List#remove(int) + */ + @Override + public DistributionSetRest remove(final int index) { + return delegate.remove(index); + } + + /** + * @param o + * @return + * @see java.util.List#indexOf(java.lang.Object) + */ + @Override + public int indexOf(final Object o) { + return delegate.indexOf(o); + } + + /** + * @param o + * @return + * @see java.util.List#lastIndexOf(java.lang.Object) + */ + @Override + public int lastIndexOf(final Object o) { + return delegate.lastIndexOf(o); + } + + /** + * @return + * @see java.util.List#listIterator() + */ + @Override + public ListIterator listIterator() { + return delegate.listIterator(); + } + + /** + * @param index + * @return + * @see java.util.List#listIterator(int) + */ + @Override + public ListIterator listIterator(final int index) { + return delegate.listIterator(index); + } + + /** + * @param fromIndex + * @param toIndex + * @return + * @see java.util.List#subList(int, int) + */ + @Override + public List subList(final int fromIndex, final int toIndex) { + return delegate.subList(fromIndex, toIndex); + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/TargetAssignmentRequestBody.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/TargetAssignmentRequestBody.java new file mode 100755 index 000000000..2e0fdefdd --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/TargetAssignmentRequestBody.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.distributionset; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Request Body of Target for assignment operations (ID only). + * + * + * + * + */ +@ApiModel("Target ID") +@JsonIgnoreProperties(ignoreUnknown = true) +public class TargetAssignmentRequestBody { + + @ApiModelProperty(value = ApiModelProperties.ITEM_ID) + @JsonProperty + private String id; + + @ApiModelProperty(value = "The force time in case type is 'timeforced'", required = false) + private long forcetime; + + @ApiModelProperty(value = "The type of action to assign, default 'forced'", required = false, allowableValues = "soft,forced,timeforced") + private ActionTypeRest type; + + /** + * @return the id + */ + public String getId() { + return id; + } + + /** + * @param id + * the id to set + */ + public void setId(final String id) { + this.id = id; + } + + /** + * @return the type + */ + public ActionTypeRest getType() { + return type; + } + + /** + * @param type + * the type to set + */ + public void setType(final ActionTypeRest type) { + this.type = type; + } + + /** + * @return the forcetime + */ + public long getForcetime() { + return forcetime; + } + + /** + * @param forcetime + * the forcetime to set + */ + public void setForcetime(final long forcetime) { + this.forcetime = forcetime; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/TargetAssignmentResponseBody.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/TargetAssignmentResponseBody.java new file mode 100755 index 000000000..8b846fe83 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionset/TargetAssignmentResponseBody.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.distributionset; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Response Body of Target for assignment operations. + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@ApiModel("Target Assignment Response") +public class TargetAssignmentResponseBody { + + @ApiModelProperty(value = "The number of targets have been assigned") + private int assigned; + @ApiModelProperty(value = "The number of targets which already have been the assignment") + private int alreadyAssigned; + @ApiModelProperty(value = "The total number of targets") + private int total; + + /** + * @return the assigned + */ + public int getAssigned() { + return assigned; + } + + /** + * @param assigned + * the assigned to set + */ + public void setAssigned(final int assigned) { + this.assigned = assigned; + } + + /** + * @return the alreadyAssigned + */ + public int getAlreadyAssigned() { + return alreadyAssigned; + } + + /** + * @param alreadyAssigned + * the alreadyAssigned to set + */ + public void setAlreadyAssigned(final int alreadyAssigned) { + this.alreadyAssigned = alreadyAssigned; + } + + /** + * @return the total + */ + public int getTotal() { + return total; + } + + /** + * @param total + * the total to set + */ + public void setTotal(final int total) { + this.total = total; + } +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionsettype/DistributionSetTypePagedList.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionsettype/DistributionSetTypePagedList.java new file mode 100755 index 000000000..a11b6627f --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionsettype/DistributionSetTypePagedList.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.distributionsettype; + +import java.util.List; + +import org.eclipse.hawkbit.rest.resource.model.PagedList; +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Paged list for DistributionSetType. + * + * + * + * + */ +@ApiModel("Paged list of distribution set types") +public class DistributionSetTypePagedList extends PagedList { + + @ApiModelProperty(value = ApiModelProperties.DS_TYPE_LIST, required = true) + private final List content; + + /** + * @param content + * @param total + */ + public DistributionSetTypePagedList(final List content, final long total) { + super(content, total); + this.content = content; + } + + /** + * @return the content of the paged list. Never {@code null}. + */ + public List getContent() { + return content; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionsettype/DistributionSetTypeRequestBodyCreate.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionsettype/DistributionSetTypeRequestBodyCreate.java new file mode 100755 index 000000000..493e6c1f7 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionsettype/DistributionSetTypeRequestBodyCreate.java @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.distributionsettype; + +import java.util.List; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; +import org.eclipse.hawkbit.rest.resource.model.softwaremoduletype.SoftwareModuleTypeAssigmentRest; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModelProperty; + +/** + * Request Body for DistributionSetType POST. + * + * + * + * + */ +public class DistributionSetTypeRequestBodyCreate { + + @ApiModelProperty(value = ApiModelProperties.NAME, required = true) + @JsonProperty(required = true) + private String name; + + @ApiModelProperty(value = ApiModelProperties.DESCRPTION) + @JsonProperty + private String description; + + @ApiModelProperty(value = ApiModelProperties.DS_TYPE_KEY) + @JsonProperty + private String key; + + @ApiModelProperty(value = ApiModelProperties.DS_TYPE_MANDATORY_MODULES) + @JsonProperty + private List mandatorymodules; + + @ApiModelProperty(value = ApiModelProperties.DS_TYPE_OPTIONAL_MODULES) + @JsonProperty + private List optionalmodules; + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name + * the name to set + */ + public void setName(final String name) { + this.name = name; + } + + /** + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * @param description + * the description to set + */ + public void setDescription(final String description) { + this.description = description; + } + + /** + * @return the key + */ + public String getKey() { + return key; + } + + /** + * @param key + * the key to set + */ + public void setKey(final String key) { + this.key = key; + } + + /** + * @return the mandatorymodules + */ + public List getMandatorymodules() { + return mandatorymodules; + } + + /** + * @param mandatorymodules + * the mandatorymodules to set + */ + public void setMandatorymodules(final List mandatorymodules) { + this.mandatorymodules = mandatorymodules; + } + + /** + * @return the optionalmodules + */ + public List getOptionalmodules() { + return optionalmodules; + } + + /** + * @param optionalmodules + * the optionalmodules to set + */ + public void setOptionalmodules(final List optionalmodules) { + this.optionalmodules = optionalmodules; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionsettype/DistributionSetTypeRequestBodyUpdate.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionsettype/DistributionSetTypeRequestBodyUpdate.java new file mode 100755 index 000000000..b5669652f --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionsettype/DistributionSetTypeRequestBodyUpdate.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.distributionsettype; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModelProperty; + +/** + * Request Body for DistributionSetType PUT, i.e. update. + * + * + * + * + */ +public class DistributionSetTypeRequestBodyUpdate { + + @ApiModelProperty(value = ApiModelProperties.DESCRPTION) + @JsonProperty + private String description; + + /** + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * @param description + * the description to set + */ + public void setDescription(final String description) { + this.description = description; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionsettype/DistributionSetTypeRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionsettype/DistributionSetTypeRest.java new file mode 100755 index 000000000..b5092dfb7 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionsettype/DistributionSetTypeRest.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.distributionsettype; + +import org.eclipse.hawkbit.rest.resource.model.NamedEntityRest; +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * A json annotated rest model for SoftwareModuleType to RESTful API + * representation. + * + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@ApiModel("Software Module") +public class DistributionSetTypeRest extends NamedEntityRest { + + @ApiModelProperty(value = ApiModelProperties.ITEM_ID, required = true) + @JsonProperty(value = "id", required = true) + private Long moduleId; + + @ApiModelProperty(value = ApiModelProperties.DS_TYPE_KEY) + @JsonProperty + private String key; + + /** + * @return the moduleId + */ + public Long getModuleId() { + return moduleId; + } + + /** + * @param moduleId + * the moduleId to set + */ + public void setModuleId(final Long moduleId) { + this.moduleId = moduleId; + } + + /** + * @return the key + */ + public String getKey() { + return key; + } + + /** + * @param key + * the key to set + */ + public void setKey(final String key) { + this.key = key; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionsettype/DistributionSetTypesRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionsettype/DistributionSetTypesRest.java new file mode 100755 index 000000000..1ba520549 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/distributionsettype/DistributionSetTypesRest.java @@ -0,0 +1,280 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.distributionsettype; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import org.springframework.hateoas.ResourceSupport; + +import io.swagger.annotations.ApiModel; + +/** + * List representation of the {@link DistributionSetTypeRest} because Spring MVC + * cannot handle plain lists interfaces as request body. + * + * + * + * + */ +@ApiModel("DistributionSetType") +public class DistributionSetTypesRest extends ResourceSupport implements List { + + private final List delegate = new ArrayList<>(); + + /** + * @return + * @see java.util.List#size() + */ + @Override + public int size() { + return delegate.size(); + } + + /** + * @return + * @see java.util.List#isEmpty() + */ + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + /** + * @param o + * @return + * @see java.util.List#contains(java.lang.Object) + */ + @Override + public boolean contains(final Object o) { + return delegate.contains(o); + } + + /** + * @return + * @see java.util.List#iterator() + */ + @Override + public Iterator iterator() { + return delegate.iterator(); + } + + /** + * @return + * @see java.util.List#toArray() + */ + @Override + public Object[] toArray() { + return delegate.toArray(); + } + + /** + * @param a + * @return + * @see java.util.List#toArray(java.lang.Object[]) + */ + @Override + public T[] toArray(final T[] a) { + return delegate.toArray(a); + } + + /** + * @param e + * @return + * @see java.util.List#add(java.lang.Object) + */ + @Override + public boolean add(final DistributionSetTypeRest e) { + return delegate.add(e); + } + + /** + * @param o + * @return + * @see java.util.List#remove(java.lang.Object) + */ + @Override + public boolean remove(final Object o) { + return delegate.remove(o); + } + + /** + * @param c + * @return + * @see java.util.List#containsAll(java.util.Collection) + */ + @Override + public boolean containsAll(final Collection c) { + return delegate.containsAll(c); + } + + /** + * @param c + * @return + * @see java.util.List#addAll(java.util.Collection) + */ + @Override + public boolean addAll(final Collection c) { + return delegate.addAll(c); + } + + /** + * @param index + * @param c + * @return + * @see java.util.List#addAll(int, java.util.Collection) + */ + @Override + public boolean addAll(final int index, final Collection c) { + return delegate.addAll(index, c); + } + + /** + * @param c + * @return + * @see java.util.List#removeAll(java.util.Collection) + */ + @Override + public boolean removeAll(final Collection c) { + return delegate.removeAll(c); + } + + /** + * @param c + * @return + * @see java.util.List#retainAll(java.util.Collection) + */ + @Override + public boolean retainAll(final Collection c) { + return delegate.retainAll(c); + } + + /** + * + * @see java.util.List#clear() + */ + @Override + public void clear() { + delegate.clear(); + } + + /** + * @param o + * @return + * @see java.util.List#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object o) { + return delegate.equals(o); + } + + /** + * @return + * @see java.util.List#hashCode() + */ + @Override + public int hashCode() { + return delegate.hashCode(); + } + + /** + * @param index + * @return + * @see java.util.List#get(int) + */ + @Override + public DistributionSetTypeRest get(final int index) { + return delegate.get(index); + } + + /** + * @param index + * @param element + * @return + * @see java.util.List#set(int, java.lang.Object) + */ + @Override + public DistributionSetTypeRest set(final int index, final DistributionSetTypeRest element) { + return delegate.set(index, element); + } + + /** + * @param index + * @param element + * @see java.util.List#add(int, java.lang.Object) + */ + @Override + public void add(final int index, final DistributionSetTypeRest element) { + delegate.add(index, element); + } + + /** + * @param index + * @return + * @see java.util.List#remove(int) + */ + @Override + public DistributionSetTypeRest remove(final int index) { + return delegate.remove(index); + } + + /** + * @param o + * @return + * @see java.util.List#indexOf(java.lang.Object) + */ + @Override + public int indexOf(final Object o) { + return delegate.indexOf(o); + } + + /** + * @param o + * @return + * @see java.util.List#lastIndexOf(java.lang.Object) + */ + @Override + public int lastIndexOf(final Object o) { + return delegate.lastIndexOf(o); + } + + /** + * @return + * @see java.util.List#listIterator() + */ + @Override + public ListIterator listIterator() { + return delegate.listIterator(); + } + + /** + * @param index + * @return + * @see java.util.List#listIterator(int) + */ + @Override + public ListIterator listIterator(final int index) { + return delegate.listIterator(index); + } + + /** + * @param fromIndex + * @param toIndex + * @return + * @see java.util.List#subList(int, int) + */ + @Override + public List subList(final int fromIndex, final int toIndex) { + return delegate.subList(fromIndex, toIndex); + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/doc/ApiModelProperties.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/doc/ApiModelProperties.java new file mode 100755 index 000000000..a9414478f --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/doc/ApiModelProperties.java @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.doc; + +/** + * Constants for API documentation. + * + * + * + * + */ +public final class ApiModelProperties { + private static final String ENDING = " of the entity"; + + // generic + public static final String ITEM_ID = "the id" + ENDING; + + public static final String NAME = "the name" + ENDING; + public static final String DESCRPTION = "the description" + ENDING; + + public static final String CREATED_BY = "entity was originally created by (IM UUID)"; + + public static final String CREATED_AT = "entity was originally created at"; + public static final String LAST_MODIFIED_BY = "entity was last modified by (IM UUID)"; + public static final String LAST_MODIFIED_AT = "entity was last modified at"; + public static final String SIZE = "current page size"; + public static final String TOTAL_ELEMENTS = "total number of elements"; + + // Versioned entity + public static final String VERSION = "software version"; + + // software module + public static final String SOFTWARE_MODULE_TYPE = "the software module type " + ENDING; + + public static final String VENDOR = "the software vendor " + ENDING; + public static final String ARTIFACT_HASHES = "hashes of the artifact"; + public static final String ARTIFACT_SIZE = "size of the artifact"; + public static final String ARTIFACT_PROVIDED_FILENAME = "filename of the artifact"; + public static final String ARTIFACT_HASHES_SHA1 = "SHA1 hash of the artifact"; + public static final String ARTIFACT_HASHES_MD5 = "MD5 hash of the artifact"; + public static final String DS_OS = "operating system or firmware software module - DEPRECATED (use modules)"; + public static final String DS_RUNTIME = "runtime software module (e.g. JVM) - DEPRECATED (use modules)"; + public static final String DS_APPLICATION = "application software module (e.g. OSGi container) - DEPRECATED (use modules)"; + public static final String DS_MODULES = "software modules (e.g. OSGi bundles, runtimes)"; + public static final String DS_REQUIRED_STEP = "is a required migration step"; + + // Target + public static final String INSTALLED_AT = "installation time of current installed DistributionSet"; + + public static final String LAST_REQUEST_AT = "last time where the target polled the server, same as pollStatus.lastRequestAt"; + // poll status + public static final String POLL_LAST_REQUEST_AT = "last time where the target polled the server"; + public static final String POLL_NEXT_EXPECTED_REQUEST_AT = "next expected time where the target polls the server"; + public static final String POLL_OVERDUE = "defines if the target poll time is overdued based on the next expected poll time plus the configured overdue poll time threshold"; + + public static final String UPDATE_STATUS = "current update status"; + public static final String TARGET_ATTRIBUTES = "target attributes"; + + public static final String TARGET_LIST = "list of targets"; + + public static final String SM_LIST = "list of software modules"; + + public static final String ACTION_TYPE = "type of action"; + + public static final String ACTION_STATUS_TYPE = "type of the action status"; + + public static final String ACTION_STATUS_MESSAGES = "messages related to the status"; + + public static final String ACTION_STATUS_REPORTED_AT = "time at which the status was reported (server time)"; + + public static final String ACTION_STATUS_LIST = "list of action status"; + + public static final String ACTION_EXECUTION_STATUS = "status of action"; + + public static final String ACTION_LIST = "list if actions"; + + public static final String POLL_STATUS = "status of the poll time of the target"; + + public static final String IP_ADDRESS = "last known ip-address of the target"; + + public static final String ADDRESS = "last known address of the target"; + + public static final String SECURITY_TOKEN = "the security token of this target which can be used to authenticate the target if enabled"; + + public static final String META_DATA = "Metadata"; + + public static final String SM_TYPE_KEY = "key that can be interpreted by the target"; + + public static final String SM_MAX_ASSIGNMENTS = "maximum number of assigments to a distribution set/target, e.g. only one firmware but multiple applications"; + + public static final String SM_TYPE_LIST = "list of software modules types"; + + // Direct Device Integration API + public static final String TARGET_TIME = "time on the target device"; + + public static final String TARGET_STATUS = "SP target action status"; + + public static final String TARGET_EXEC_STATUS = "status of the action execution"; + + public static final String TARGET_RESULT_VALUE = "result of the action execution"; + + public static final String TARGET_RESULT_DETAILS = "List of details message information"; + + public static final String TARGET_RESULT_FINISHED = "defined status of the result"; + + public static final String TARGET_RESULT_PROGRESS = "progress assumption of the device"; + + public static final String TARGET_PROGRESS_CNT = "current progress level"; + + public static final String TARGET_PROGRESS_OF = "asumption concerning max progress level"; + + public static final String ACTION_ID = "id of the action"; + + public static final String CANCEL_ACTION = "action that needs to be canceled"; + + public static final String CHUNK_TYPE = "type of the chunk, e.g. firmware, bundle, app"; + + public static final String SOFTWARE_MODUL_TYPE = "type of the software module, e.g. firmware, bundle, app"; + + public static final String SOFTWARE_MODULE_VERSION = "version of the software module"; + + public static final String SOFTWARE_MODULE_NAME = "name of the software module"; + + public static final String SOFTWARE_MODULE_ARTIFACT_LINKS = "artifact links of the software module"; + + public static final String SOFTWARE_MODUL_ID = "id of the software module"; + + public static final String CHUNK_VERSION = "software version of the chunk"; + + public static final String CHUNK_NAME = "name of the chunk"; + + public static final String ARTIFACTS = "list of artifacts"; + + public static final String TARGET_CONFIGURATION = "target configuration setup by the server"; + + public static final String TARGET_POLL_TIME = "suggested sleep time between polls"; + + public static final String TARGET_SLEEP = "sleep time in HH:MM:SS notation"; + + public static final String DEPLOYMENT = "detailed deployment operation"; + + public static final String HANDLING_DOWNLOAD = "handling for the download part of the provisioning process"; + + public static final String HANDLING_UPDATE = "handling for the update part of the provisioning process"; + + public static final String CHUNK = "software chunk of an update"; + + public static final String SOFTWARE_MODUL = "software moduls of an update"; + + public static final String ARTIFACT = "artifact moduls of an update"; + + public static final String TARGET_CONFIG_DATA = "configuration data as key/value list"; + + public static final String DS_TYPE_KEY = "funtional key of the distribution set type"; + + public static final String DS_TYPE_LIST = "list of distribution set types"; + + public static final String DS_TYPE = "the type of the distribution set"; + + public static final String DS_COMPLETE = "true of the distribution set software module setup is complete by means as defined by the distribution set type"; + + public static final String DS_TYPE_MANDATORY_MODULES = "mandatory module type IDs"; + + public static final String DS_TYPE_OPTIONAL_MODULES = "optional module type IDs"; + + private ApiModelProperties() { + // utility class + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModuleAssigmentRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModuleAssigmentRest.java new file mode 100755 index 000000000..378b4e644 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModuleAssigmentRest.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.softwaremodule; + +import org.eclipse.hawkbit.rest.resource.model.IdRest; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import io.swagger.annotations.ApiModel; + +/** + * Request Body of SoftwareModule for assignment operations (ID only). + * + * + * + * + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@ApiModel("Software Module ID") +public class SoftwareModuleAssigmentRest extends IdRest { + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModulePagedList.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModulePagedList.java new file mode 100755 index 000000000..1bba983ea --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModulePagedList.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.softwaremodule; + +import java.util.List; + +import org.eclipse.hawkbit.rest.resource.model.PagedList; +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Paged list for SoftwareModule. + * + * + * + * + */ +@ApiModel("Paged list of software modules") +public class SoftwareModulePagedList extends PagedList { + + @ApiModelProperty(value = ApiModelProperties.SM_LIST, required = true) + private final List content; + + /** + * @param content + * @param total + */ + public SoftwareModulePagedList(final List content, final long total) { + super(content, total); + this.content = content; + } + + /** + * @return the content of the paged list. Never {@code null}. + */ + public List getContent() { + return content; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModuleRequestBodyPost.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModuleRequestBodyPost.java new file mode 100755 index 000000000..13401c1f4 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModuleRequestBodyPost.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.softwaremodule; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModelProperty; + +/** + * Request Body for SoftwareModule POST. + * + * + * + * + */ +public class SoftwareModuleRequestBodyPost { + + @ApiModelProperty(value = ApiModelProperties.NAME, required = true) + @JsonProperty(required = true) + private String name; + + @ApiModelProperty(value = ApiModelProperties.VERSION, required = true) + @JsonProperty(required = true) + private String version; + + @ApiModelProperty(value = ApiModelProperties.SOFTWARE_MODULE_TYPE, required = true, allowableValues = SoftwareModuleRest.SM_RUNTIME + + "," + SoftwareModuleRest.SM_APPLICATION + "," + SoftwareModuleRest.SM_OS) + @JsonProperty(required = true) + private String type; + + @ApiModelProperty(value = ApiModelProperties.DESCRPTION) + @JsonProperty + private String description; + + @ApiModelProperty(value = ApiModelProperties.VENDOR) + @JsonProperty + private String vendor; + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name + * the name to set + */ + public void setName(final String name) { + this.name = name; + } + + /** + * @return the version + */ + public String getVersion() { + return version; + } + + /** + * @param version + * the version to set + */ + public void setVersion(final String version) { + this.version = version; + } + + /** + * @return the type + */ + public String getType() { + return type; + } + + /** + * @param type + * the type to set + */ + public void setType(final String type) { + this.type = type; + } + + /** + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * @param description + * the description to set + */ + public void setDescription(final String description) { + this.description = description; + } + + /** + * @return the vendor + */ + public String getVendor() { + return vendor; + } + + /** + * @param vendor + * the vendor to set + */ + public void setVendor(final String vendor) { + this.vendor = vendor; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModuleRequestBodyPut.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModuleRequestBodyPut.java new file mode 100755 index 000000000..310b0a942 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModuleRequestBodyPut.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.softwaremodule; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModelProperty; + +/** + * Request Body for SoftwareModule PUT. + * + * + * + * + */ +public class SoftwareModuleRequestBodyPut { + + @ApiModelProperty(value = ApiModelProperties.DESCRPTION) + @JsonProperty + private String description; + + @ApiModelProperty(value = ApiModelProperties.VENDOR) + @JsonProperty + private String vendor; + + /** + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * @param description + * the description to set + */ + public void setDescription(final String description) { + this.description = description; + } + + /** + * @return the vendor + */ + public String getVendor() { + return vendor; + } + + /** + * @param vendor + * the vendor to set + */ + public void setVendor(final String vendor) { + this.vendor = vendor; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModuleRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModuleRest.java new file mode 100755 index 000000000..0e1ee60fb --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModuleRest.java @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.softwaremodule; + +import org.eclipse.hawkbit.rest.resource.model.NamedEntityRest; +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * A json annotated rest model for SoftwareModule to RESTful API representation. + * + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@ApiModel("Software Module") +public class SoftwareModuleRest extends NamedEntityRest { + /** + * API definition for {@link SoftwareModule.Type#RUNTIME}. + */ + public static final String SM_RUNTIME = "runtime"; + /** + * API definition for {@link SoftwareModule.Type#OS}. + */ + public static final String SM_OS = "os"; + /** + * API definition for {@link SoftwareModule.Type#APPLICATION}. + */ + public static final String SM_APPLICATION = "application"; + + @ApiModelProperty(value = ApiModelProperties.ITEM_ID, required = true) + @JsonProperty(value = "id", required = true) + private Long moduleId; + + @ApiModelProperty(value = ApiModelProperties.VERSION, required = true) + @JsonProperty(required = true) + private String version; + + @ApiModelProperty(value = ApiModelProperties.SOFTWARE_MODULE_TYPE, required = true) + @JsonProperty(required = true) + private String type; + + @ApiModelProperty(value = ApiModelProperties.VENDOR) + @JsonProperty + private String vendor; + + /** + * @return the moduleId + */ + public Long getModuleId() { + return moduleId; + } + + /** + * @param moduleId + * the moduleId to set + */ + @JsonIgnore + public void setModuleId(final Long moduleId) { + this.moduleId = moduleId; + } + + /** + * @return the version + */ + public String getVersion() { + return version; + } + + /** + * @param version + * the version to set + */ + public void setVersion(final String version) { + this.version = version; + } + + /** + * @return the type + */ + public String getType() { + return type; + } + + /** + * @param type + * the type to set + */ + public void setType(final String type) { + this.type = type; + } + + /** + * @return the vendor + */ + public String getVendor() { + return vendor; + } + + /** + * @param vendor + * the vendor to set + */ + public void setVendor(final String vendor) { + this.vendor = vendor; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModulesRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModulesRest.java new file mode 100755 index 000000000..b40c98d36 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremodule/SoftwareModulesRest.java @@ -0,0 +1,277 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.softwaremodule; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import org.springframework.hateoas.ResourceSupport; + +/** + * List representation of the {@link SoftwareModuleRest} because Spring MVC + * cannot handle plain lists interfaces as request body. + * + * + * + * + */ +public class SoftwareModulesRest extends ResourceSupport implements List { + + private final List delegate = new ArrayList<>(); + + /** + * @return + * @see java.util.List#size() + */ + @Override + public int size() { + return delegate.size(); + } + + /** + * @return + * @see java.util.List#isEmpty() + */ + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + /** + * @param o + * @return + * @see java.util.List#contains(java.lang.Object) + */ + @Override + public boolean contains(final Object o) { + return delegate.contains(o); + } + + /** + * @return + * @see java.util.List#iterator() + */ + @Override + public Iterator iterator() { + return delegate.iterator(); + } + + /** + * @return + * @see java.util.List#toArray() + */ + @Override + public Object[] toArray() { + return delegate.toArray(); + } + + /** + * @param a + * @return + * @see java.util.List#toArray(java.lang.Object[]) + */ + @Override + public T[] toArray(final T[] a) { + return delegate.toArray(a); + } + + /** + * @param e + * @return + * @see java.util.List#add(java.lang.Object) + */ + @Override + public boolean add(final SoftwareModuleRest e) { + return delegate.add(e); + } + + /** + * @param o + * @return + * @see java.util.List#remove(java.lang.Object) + */ + @Override + public boolean remove(final Object o) { + return delegate.remove(o); + } + + /** + * @param c + * @return + * @see java.util.List#containsAll(java.util.Collection) + */ + @Override + public boolean containsAll(final Collection c) { + return delegate.containsAll(c); + } + + /** + * @param c + * @return + * @see java.util.List#addAll(java.util.Collection) + */ + @Override + public boolean addAll(final Collection c) { + return delegate.addAll(c); + } + + /** + * @param index + * @param c + * @return + * @see java.util.List#addAll(int, java.util.Collection) + */ + @Override + public boolean addAll(final int index, final Collection c) { + return delegate.addAll(index, c); + } + + /** + * @param c + * @return + * @see java.util.List#removeAll(java.util.Collection) + */ + @Override + public boolean removeAll(final Collection c) { + return delegate.removeAll(c); + } + + /** + * @param c + * @return + * @see java.util.List#retainAll(java.util.Collection) + */ + @Override + public boolean retainAll(final Collection c) { + return delegate.retainAll(c); + } + + /** + * + * @see java.util.List#clear() + */ + @Override + public void clear() { + delegate.clear(); + } + + /** + * @param o + * @return + * @see java.util.List#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object o) { + return delegate.equals(o); + } + + /** + * @return + * @see java.util.List#hashCode() + */ + @Override + public int hashCode() { + return delegate.hashCode(); + } + + /** + * @param index + * @return + * @see java.util.List#get(int) + */ + @Override + public SoftwareModuleRest get(final int index) { + return delegate.get(index); + } + + /** + * @param index + * @param element + * @return + * @see java.util.List#set(int, java.lang.Object) + */ + @Override + public SoftwareModuleRest set(final int index, final SoftwareModuleRest element) { + return delegate.set(index, element); + } + + /** + * @param index + * @param element + * @see java.util.List#add(int, java.lang.Object) + */ + @Override + public void add(final int index, final SoftwareModuleRest element) { + delegate.add(index, element); + } + + /** + * @param index + * @return + * @see java.util.List#remove(int) + */ + @Override + public SoftwareModuleRest remove(final int index) { + return delegate.remove(index); + } + + /** + * @param o + * @return + * @see java.util.List#indexOf(java.lang.Object) + */ + @Override + public int indexOf(final Object o) { + return delegate.indexOf(o); + } + + /** + * @param o + * @return + * @see java.util.List#lastIndexOf(java.lang.Object) + */ + @Override + public int lastIndexOf(final Object o) { + return delegate.lastIndexOf(o); + } + + /** + * @return + * @see java.util.List#listIterator() + */ + @Override + public ListIterator listIterator() { + return delegate.listIterator(); + } + + /** + * @param index + * @return + * @see java.util.List#listIterator(int) + */ + @Override + public ListIterator listIterator(final int index) { + return delegate.listIterator(index); + } + + /** + * @param fromIndex + * @param toIndex + * @return + * @see java.util.List#subList(int, int) + */ + @Override + public List subList(final int fromIndex, final int toIndex) { + return delegate.subList(fromIndex, toIndex); + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypeAssigmentRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypeAssigmentRest.java new file mode 100755 index 000000000..892c86b49 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypeAssigmentRest.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.softwaremoduletype; + +import org.eclipse.hawkbit.rest.resource.model.IdRest; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import io.swagger.annotations.ApiModel; + +/** + * Request Body of SoftwareModuleType for assignment operations (ID only). + * + * + * + * + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@ApiModel("Software Module Type ID") +public class SoftwareModuleTypeAssigmentRest extends IdRest { + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypePagedList.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypePagedList.java new file mode 100755 index 000000000..b5df0837f --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypePagedList.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.softwaremoduletype; + +import java.util.List; + +import org.eclipse.hawkbit.rest.resource.model.PagedList; +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Paged list for SoftwareModuleType. + * + * + * + * + */ +@ApiModel("Paged list of software module typess") +public class SoftwareModuleTypePagedList extends PagedList { + + @ApiModelProperty(value = ApiModelProperties.SM_TYPE_LIST, required = true) + private final List content; + + /** + * @param content + * @param total + */ + public SoftwareModuleTypePagedList(final List content, final long total) { + super(content, total); + this.content = content; + } + + /** + * @return the content of the paged list. Never {@code null}. + */ + public List getContent() { + return content; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypeRequestBodyPost.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypeRequestBodyPost.java new file mode 100755 index 000000000..ec5d9e1a1 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypeRequestBodyPost.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.softwaremoduletype; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModelProperty; + +/** + * Request Body for SoftwareModuleType POST. + * + * + * + * + */ +public class SoftwareModuleTypeRequestBodyPost { + + @ApiModelProperty(value = ApiModelProperties.NAME, required = true) + @JsonProperty(required = true) + private String name; + + @ApiModelProperty(value = ApiModelProperties.DESCRPTION) + @JsonProperty + private String description; + + @ApiModelProperty(value = ApiModelProperties.SM_TYPE_KEY) + @JsonProperty + private String key; + + @ApiModelProperty(value = ApiModelProperties.SM_MAX_ASSIGNMENTS) + @JsonProperty + private int maxAssignments; + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name + * the name to set + */ + public void setName(final String name) { + this.name = name; + } + + /** + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * @param description + * the description to set + */ + public void setDescription(final String description) { + this.description = description; + } + + /** + * @return the key + */ + public String getKey() { + return key; + } + + /** + * @param key + * the key to set + */ + public void setKey(final String key) { + this.key = key; + } + + /** + * @return the maxAssignments + */ + public int getMaxAssignments() { + return maxAssignments; + } + + /** + * @param maxAssignments + * the maxAssignments to set + */ + public void setMaxAssignments(final int maxAssignments) { + this.maxAssignments = maxAssignments; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypeRequestBodyPut.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypeRequestBodyPut.java new file mode 100755 index 000000000..78c9a7973 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypeRequestBodyPut.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.softwaremoduletype; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModelProperty; + +/** + * Request Body for SoftwareModuleType PUT. + * + * + * + * + */ +public class SoftwareModuleTypeRequestBodyPut { + + @ApiModelProperty(value = ApiModelProperties.DESCRPTION) + @JsonProperty + private String description; + + /** + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * @param description + * the description to set + */ + public void setDescription(final String description) { + this.description = description; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypeRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypeRest.java new file mode 100755 index 000000000..8e558cc2a --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypeRest.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.softwaremoduletype; + +import org.eclipse.hawkbit.rest.resource.model.NamedEntityRest; +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * A json annotated rest model for SoftwareModuleType to RESTful API + * representation. + * + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@ApiModel("Software Module") +public class SoftwareModuleTypeRest extends NamedEntityRest { + + @ApiModelProperty(value = ApiModelProperties.ITEM_ID, required = true) + @JsonProperty(value = "id", required = true) + private Long moduleId; + + @ApiModelProperty(value = ApiModelProperties.SM_TYPE_KEY) + @JsonProperty + private String key; + + @ApiModelProperty(value = ApiModelProperties.SM_MAX_ASSIGNMENTS) + @JsonProperty + private int maxAssignments; + + /** + * @return the moduleId + */ + public Long getModuleId() { + return moduleId; + } + + /** + * @param moduleId + * the moduleId to set + */ + public void setModuleId(final Long moduleId) { + this.moduleId = moduleId; + } + + /** + * @return the key + */ + public String getKey() { + return key; + } + + /** + * @param key + * the key to set + */ + public void setKey(final String key) { + this.key = key; + } + + /** + * @return the maxAssignments + */ + public int getMaxAssignments() { + return maxAssignments; + } + + /** + * @param maxAssignments + * the maxAssignments to set + */ + public void setMaxAssignments(final int maxAssignments) { + this.maxAssignments = maxAssignments; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypesRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypesRest.java new file mode 100755 index 000000000..c3f13b42a --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/softwaremoduletype/SoftwareModuleTypesRest.java @@ -0,0 +1,280 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.softwaremoduletype; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import org.springframework.hateoas.ResourceSupport; + +import io.swagger.annotations.ApiModel; + +/** + * List representation of the {@link SoftwareModuleTypeRest} because Spring MVC + * cannot handle plain lists interfaces as request body. + * + * + * + * + */ +@ApiModel("SoftwareModuleType") +public class SoftwareModuleTypesRest extends ResourceSupport implements List { + + private final List delegate = new ArrayList<>(); + + /** + * @return + * @see java.util.List#size() + */ + @Override + public int size() { + return delegate.size(); + } + + /** + * @return + * @see java.util.List#isEmpty() + */ + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + /** + * @param o + * @return + * @see java.util.List#contains(java.lang.Object) + */ + @Override + public boolean contains(final Object o) { + return delegate.contains(o); + } + + /** + * @return + * @see java.util.List#iterator() + */ + @Override + public Iterator iterator() { + return delegate.iterator(); + } + + /** + * @return + * @see java.util.List#toArray() + */ + @Override + public Object[] toArray() { + return delegate.toArray(); + } + + /** + * @param a + * @return + * @see java.util.List#toArray(java.lang.Object[]) + */ + @Override + public T[] toArray(final T[] a) { + return delegate.toArray(a); + } + + /** + * @param e + * @return + * @see java.util.List#add(java.lang.Object) + */ + @Override + public boolean add(final SoftwareModuleTypeRest e) { + return delegate.add(e); + } + + /** + * @param o + * @return + * @see java.util.List#remove(java.lang.Object) + */ + @Override + public boolean remove(final Object o) { + return delegate.remove(o); + } + + /** + * @param c + * @return + * @see java.util.List#containsAll(java.util.Collection) + */ + @Override + public boolean containsAll(final Collection c) { + return delegate.containsAll(c); + } + + /** + * @param c + * @return + * @see java.util.List#addAll(java.util.Collection) + */ + @Override + public boolean addAll(final Collection c) { + return delegate.addAll(c); + } + + /** + * @param index + * @param c + * @return + * @see java.util.List#addAll(int, java.util.Collection) + */ + @Override + public boolean addAll(final int index, final Collection c) { + return delegate.addAll(index, c); + } + + /** + * @param c + * @return + * @see java.util.List#removeAll(java.util.Collection) + */ + @Override + public boolean removeAll(final Collection c) { + return delegate.removeAll(c); + } + + /** + * @param c + * @return + * @see java.util.List#retainAll(java.util.Collection) + */ + @Override + public boolean retainAll(final Collection c) { + return delegate.retainAll(c); + } + + /** + * + * @see java.util.List#clear() + */ + @Override + public void clear() { + delegate.clear(); + } + + /** + * @param o + * @return + * @see java.util.List#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object o) { + return delegate.equals(o); + } + + /** + * @return + * @see java.util.List#hashCode() + */ + @Override + public int hashCode() { + return delegate.hashCode(); + } + + /** + * @param index + * @return + * @see java.util.List#get(int) + */ + @Override + public SoftwareModuleTypeRest get(final int index) { + return delegate.get(index); + } + + /** + * @param index + * @param element + * @return + * @see java.util.List#set(int, java.lang.Object) + */ + @Override + public SoftwareModuleTypeRest set(final int index, final SoftwareModuleTypeRest element) { + return delegate.set(index, element); + } + + /** + * @param index + * @param element + * @see java.util.List#add(int, java.lang.Object) + */ + @Override + public void add(final int index, final SoftwareModuleTypeRest element) { + delegate.add(index, element); + } + + /** + * @param index + * @return + * @see java.util.List#remove(int) + */ + @Override + public SoftwareModuleTypeRest remove(final int index) { + return delegate.remove(index); + } + + /** + * @param o + * @return + * @see java.util.List#indexOf(java.lang.Object) + */ + @Override + public int indexOf(final Object o) { + return delegate.indexOf(o); + } + + /** + * @param o + * @return + * @see java.util.List#lastIndexOf(java.lang.Object) + */ + @Override + public int lastIndexOf(final Object o) { + return delegate.lastIndexOf(o); + } + + /** + * @return + * @see java.util.List#listIterator() + */ + @Override + public ListIterator listIterator() { + return delegate.listIterator(); + } + + /** + * @param index + * @return + * @see java.util.List#listIterator(int) + */ + @Override + public ListIterator listIterator(final int index) { + return delegate.listIterator(index); + } + + /** + * @param fromIndex + * @param toIndex + * @return + * @see java.util.List#subList(int, int) + */ + @Override + public List subList(final int fromIndex, final int toIndex) { + return delegate.subList(fromIndex, toIndex); + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/system/CacheRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/system/CacheRest.java new file mode 100755 index 000000000..e430cba1c --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/system/CacheRest.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.system; + +import java.util.Collection; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +/** + * Model representation of an Cache entry as json. + * + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class CacheRest { + + private final String name; + private final Collection keys; + + /** + * @param name + * the name of the cache + * @param cacheKeys + * the keys which contains in the cache + */ + public CacheRest(final String name, final Collection cacheKeys) { + this.name = name; + this.keys = cacheKeys; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @return the keys + */ + public Collection getKeys() { + return keys; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "CacheRest [name=" + name + ", keys=" + keys + "]"; + } +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/system/TenantConfigurationRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/system/TenantConfigurationRest.java new file mode 100755 index 000000000..df0cc8756 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/system/TenantConfigurationRest.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model.system; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +import io.swagger.annotations.ApiModel; + +/** + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@ApiModel("Tenant Configuration") +public class TenantConfigurationRest { + + private String key; + private String value; + + /** + * @return the key + */ + public String getKey() { + return key; + } + + /** + * @param key + * the key to set + */ + public void setKey(final String key) { + this.key = key; + } + + /** + * @return the value + */ + public String getValue() { + return value; + } + + /** + * @param value + * the value to set + */ + public void setValue(final String value) { + this.value = value; + } +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/DistributionSetAssigmentRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/DistributionSetAssigmentRest.java new file mode 100755 index 000000000..f0dfaca76 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/DistributionSetAssigmentRest.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2011-2015 Bosch Software Innovations GmbH, Germany. All rights reserved. + */ +package org.eclipse.hawkbit.rest.resource.model.target; + +import org.eclipse.hawkbit.rest.resource.model.IdRest; +import org.eclipse.hawkbit.rest.resource.model.distributionset.ActionTypeRest; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Request Body of DistributionSet for assignment operations (ID only). + * + * + * + * + */ +@ApiModel("Distribution Set Assignment") +public class DistributionSetAssigmentRest extends IdRest { + + @ApiModelProperty(value = "forcetime in milliseconds") + private long forcetime; + @ApiModelProperty(value = "the assignment type default 'forced'") + private ActionTypeRest type; + + /** + * @return the type + */ + public ActionTypeRest getType() { + return type; + } + + /** + * @param type + * the type to set + */ + public void setType(final ActionTypeRest type) { + this.type = type; + } + + /** + * @return the forcetime + */ + public long getForcetime() { + return forcetime; + } + + /** + * @param forcetime + * the forcetime to set + */ + public void setForcetime(final long forcetime) { + this.forcetime = forcetime; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/TargetAttributes.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/TargetAttributes.java new file mode 100755 index 000000000..e10e884d6 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/TargetAttributes.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2011-2015 Bosch Software Innovations GmbH, Germany. All rights reserved. + */ +package org.eclipse.hawkbit.rest.resource.model.target; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import io.swagger.annotations.ApiModel; + +/** + * {@link Map} with attribtes of SP Target. + * + * + * + * + */ +@ApiModel(ApiModelProperties.TARGET_ATTRIBUTES) +public class TargetAttributes extends HashMap { + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/TargetPagedList.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/TargetPagedList.java new file mode 100755 index 000000000..a83d9a363 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/TargetPagedList.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2011-2015 Bosch Software Innovations GmbH, Germany. All rights reserved. + */ +package org.eclipse.hawkbit.rest.resource.model.target; + +import java.util.List; + +import org.eclipse.hawkbit.rest.resource.model.PagedList; +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Paged list for targets. + * + * + * + * + */ +@ApiModel("Paged list of targets") +public class TargetPagedList extends PagedList { + + @ApiModelProperty(value = ApiModelProperties.TARGET_LIST, required = true) + private final List content; + + /** + * @param content + * @param total + */ + public TargetPagedList(final List content, final long total) { + super(content, total); + this.content = content; + } + + /** + * @return the content of the paged list. Never {@code null}. + */ + public List getContent() { + return content; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/TargetRequestBody.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/TargetRequestBody.java new file mode 100755 index 000000000..b3584742b --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/TargetRequestBody.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2011-2015 Bosch Software Innovations GmbH, Germany. All rights reserved. + */ +package org.eclipse.hawkbit.rest.resource.model.target; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModelProperty; + +/** + * Request body for target PUT/POST commands. + * + * + * + * + */ +public class TargetRequestBody { + @ApiModelProperty(value = ApiModelProperties.NAME, required = true) + @JsonProperty(required = true) + private String name; + + @ApiModelProperty(value = ApiModelProperties.DESCRPTION) + private String description; + + @ApiModelProperty(value = ApiModelProperties.ITEM_ID, required = true) + @JsonProperty(required = true) + private String controllerId; + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * @return the controllerId + */ + public String getControllerId() { + return controllerId; + } + + /** + * @param name + * the name to set + */ + public void setName(final String name) { + this.name = name; + } + + /** + * @param description + * the description to set + */ + public void setDescription(final String description) { + this.description = description; + } + + /** + * @param controllerId + * the controllerId to set + */ + public void setControllerId(final String controllerId) { + this.controllerId = controllerId; + } + +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/TargetRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/TargetRest.java new file mode 100755 index 000000000..bf7433485 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/TargetRest.java @@ -0,0 +1,186 @@ +/** + * Copyright (c) 2011-2015 Bosch Software Innovations GmbH, Germany. All rights reserved. + */ +package org.eclipse.hawkbit.rest.resource.model.target; + +import java.net.URI; + +import org.eclipse.hawkbit.rest.resource.model.NamedEntityRest; +import org.eclipse.hawkbit.rest.resource.model.PollStatusRest; +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * A json annotated rest model for Target to RESTful API representation. + * + * + * + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@ApiModel("Provisioning Target") +public class TargetRest extends NamedEntityRest { + + @ApiModelProperty(value = ApiModelProperties.ITEM_ID, required = true) + @JsonProperty(required = true) + private String controllerId; + + @ApiModelProperty(value = ApiModelProperties.UPDATE_STATUS, allowableValues = "error, in_sync, pending, registered, unknown") + @JsonProperty + private String updateStatus; + + @ApiModelProperty(value = ApiModelProperties.LAST_REQUEST_AT) + @JsonProperty + private Long lastControllerRequestAt; + + @ApiModelProperty(value = ApiModelProperties.INSTALLED_AT) + @JsonProperty + private Long installedAt; + + @ApiModelProperty(value = ApiModelProperties.IP_ADDRESS) + @JsonProperty + private String ipAddress; + + @ApiModelProperty(value = ApiModelProperties.ADDRESS) + @JsonProperty + private String address; + + @ApiModelProperty(value = ApiModelProperties.POLL_STATUS) + @JsonProperty + private PollStatusRest pollStatus; + + @ApiModelProperty(value = ApiModelProperties.SECURITY_TOKEN) + @JsonProperty + private String securityToken; + + /** + * @return the controllerId + */ + public String getControllerId() { + return controllerId; + } + + /** + * @param controllerId + * the controllerId to set + */ + public void setControllerId(final String controllerId) { + this.controllerId = controllerId; + } + + /** + * @return the updateStatus + */ + public String getUpdateStatus() { + return updateStatus; + } + + /** + * @param updateStatus + * the updateStatus to set + */ + public void setUpdateStatus(final String updateStatus) { + this.updateStatus = updateStatus; + } + + /** + * @return the lastControllerRequestAt + */ + public Long getLastControllerRequestAt() { + return lastControllerRequestAt; + } + + /** + * @param lastControllerRequestAt + * the lastControllerRequestAt to set + */ + @JsonIgnore + public void setLastControllerRequestAt(final Long lastControllerRequestAt) { + this.lastControllerRequestAt = lastControllerRequestAt; + } + + /** + * @return the installedAt + */ + public Long getInstalledAt() { + return installedAt; + } + + /** + * @param installedAt + * the installedAt to set + */ + @JsonIgnore + public void setInstalledAt(final Long installedAt) { + this.installedAt = installedAt; + } + + /** + * @return the pollStatus + */ + public PollStatusRest getPollStatus() { + return pollStatus; + } + + /** + * @param pollStatus + * the pollStatus to set + */ + @JsonIgnore + public void setPollStatus(final PollStatusRest pollStatus) { + this.pollStatus = pollStatus; + } + + /** + * @return the ipAddress + */ + public String getIpAddress() { + return ipAddress; + } + + /** + * @param ipAddress + * the ipAddress to set + */ + @JsonIgnore + public void setIpAddress(final String ipAddress) { + this.ipAddress = ipAddress; + } + + /** + * @return the securityToken + */ + public String getSecurityToken() { + return securityToken; + } + + public String getAddress() { + return address; + } + + @JsonIgnore + public void setAddress(final String address) { + if (address != null) { + URI.create(address); + } + this.address = address; + } + + /** + * @param securityToken + * the securityToken to set + */ + @JsonIgnore + public void setSecurityToken(final String securityToken) { + this.securityToken = securityToken; + } +} diff --git a/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/TargetsRest.java b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/TargetsRest.java new file mode 100755 index 000000000..c946e0594 --- /dev/null +++ b/hawkbit-rest-api/src/main/java/org/eclipse/hawkbit/rest/resource/model/target/TargetsRest.java @@ -0,0 +1,272 @@ +/** + * Copyright (c) 2011-2015 Bosch Software Innovations GmbH, Germany. All rights reserved. + */ +package org.eclipse.hawkbit.rest.resource.model.target; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import org.springframework.hateoas.ResourceSupport; + +/** + * A list representation of the {@link TargetRest} because Spring MVC cannot + * handle plain lists interfaces as request body. + * + * + * + * + */ +public class TargetsRest extends ResourceSupport implements List { + + private final List delegate = new ArrayList<>(); + + /** + * @return + * @see java.util.List#size() + */ + @Override + public int size() { + return delegate.size(); + } + + /** + * @return + * @see java.util.List#isEmpty() + */ + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + /** + * @param o + * @return + * @see java.util.List#contains(java.lang.Object) + */ + @Override + public boolean contains(final Object o) { + return delegate.contains(o); + } + + /** + * @return + * @see java.util.List#iterator() + */ + @Override + public Iterator iterator() { + return delegate.iterator(); + } + + /** + * @return + * @see java.util.List#toArray() + */ + @Override + public Object[] toArray() { + return delegate.toArray(); + } + + /** + * @param a + * @return + * @see java.util.List#toArray(java.lang.Object[]) + */ + @Override + public T[] toArray(final T[] a) { + return delegate.toArray(a); + } + + /** + * @param e + * @return + * @see java.util.List#add(java.lang.Object) + */ + @Override + public boolean add(final TargetRest e) { + return delegate.add(e); + } + + /** + * @param o + * @return + * @see java.util.List#remove(java.lang.Object) + */ + @Override + public boolean remove(final Object o) { + return delegate.remove(o); + } + + /** + * @param c + * @return + * @see java.util.List#containsAll(java.util.Collection) + */ + @Override + public boolean containsAll(final Collection c) { + return delegate.containsAll(c); + } + + /** + * @param c + * @return + * @see java.util.List#addAll(java.util.Collection) + */ + @Override + public boolean addAll(final Collection c) { + return delegate.addAll(c); + } + + /** + * @param index + * @param c + * @return + * @see java.util.List#addAll(int, java.util.Collection) + */ + @Override + public boolean addAll(final int index, final Collection c) { + return delegate.addAll(index, c); + } + + /** + * @param c + * @return + * @see java.util.List#removeAll(java.util.Collection) + */ + @Override + public boolean removeAll(final Collection c) { + return delegate.removeAll(c); + } + + /** + * @param c + * @return + * @see java.util.List#retainAll(java.util.Collection) + */ + @Override + public boolean retainAll(final Collection c) { + return delegate.retainAll(c); + } + + /** + * + * @see java.util.List#clear() + */ + @Override + public void clear() { + delegate.clear(); + } + + /** + * @param o + * @return + * @see java.util.List#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object o) { + return delegate.equals(o); + } + + /** + * @return + * @see java.util.List#hashCode() + */ + @Override + public int hashCode() { + return delegate.hashCode(); + } + + /** + * @param index + * @return + * @see java.util.List#get(int) + */ + @Override + public TargetRest get(final int index) { + return delegate.get(index); + } + + /** + * @param index + * @param element + * @return + * @see java.util.List#set(int, java.lang.Object) + */ + @Override + public TargetRest set(final int index, final TargetRest element) { + return delegate.set(index, element); + } + + /** + * @param index + * @param element + * @see java.util.List#add(int, java.lang.Object) + */ + @Override + public void add(final int index, final TargetRest element) { + delegate.add(index, element); + } + + /** + * @param index + * @return + * @see java.util.List#remove(int) + */ + @Override + public TargetRest remove(final int index) { + return delegate.remove(index); + } + + /** + * @param o + * @return + * @see java.util.List#indexOf(java.lang.Object) + */ + @Override + public int indexOf(final Object o) { + return delegate.indexOf(o); + } + + /** + * @param o + * @return + * @see java.util.List#lastIndexOf(java.lang.Object) + */ + @Override + public int lastIndexOf(final Object o) { + return delegate.lastIndexOf(o); + } + + /** + * @return + * @see java.util.List#listIterator() + */ + @Override + public ListIterator listIterator() { + return delegate.listIterator(); + } + + /** + * @param index + * @return + * @see java.util.List#listIterator(int) + */ + @Override + public ListIterator listIterator(final int index) { + return delegate.listIterator(index); + } + + /** + * @param fromIndex + * @param toIndex + * @return + * @see java.util.List#subList(int, int) + */ + @Override + public List subList(final int fromIndex, final int toIndex) { + return delegate.subList(fromIndex, toIndex); + } + +} diff --git a/hawkbit-rest-resource/pom.xml b/hawkbit-rest-resource/pom.xml new file mode 100755 index 000000000..068dfd521 --- /dev/null +++ b/hawkbit-rest-resource/pom.xml @@ -0,0 +1,164 @@ + + + 4.0.0 + + org.eclipse.hawkbit + hawkbit-parent + 0.1.0-SNAPSHOT + + hawkbit-rest-resource + hawkBit :: REST Resources + + + + + org.eclipse.hawkbit + hawkbit-repository + ${project.version} + + + org.eclipse.hawkbit + hawkbit-rest-api + ${project.version} + + + javax.servlet + javax.servlet-api + provided + + + org.apache.tomcat.embed + tomcat-embed-core + provided + + + io.springfox + springfox-core + 2.0.3 + + + commons-io + commons-io + 2.4 + + + org.springframework.security + spring-security-web + + + + + com.h2database + h2 + test + + + org.mariadb.jdbc + mariadb-java-client + test + + + javax.el + javax.el-api + test + + + org.springframework.security + spring-security-config + test + + + org.eclipse.hawkbit + hawkbit-repository + ${project.version} + tests + test + + + org.eclipse.hawkbit + hawkbit-http-security + ${project.version} + test + + + com.jayway.jsonpath + json-path + test + + + org.json + json + test + + + com.fasterxml.jackson.core + jackson-databind + test + + + com.fasterxml.jackson.core + jackson-core + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-aspects + test + + + org.easytesting + fest-assert-core + test + + + org.easytesting + fest-assert + test + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + test + + + ru.yandex.qatools.allure + allure-junit-adaptor + test + + + org.springframework + spring-context-support + test + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + \ No newline at end of file diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/cache/CacheWriteNotify.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/cache/CacheWriteNotify.java new file mode 100755 index 000000000..1bf5704e1 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/cache/CacheWriteNotify.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.cache; + +import org.eclipse.hawkbit.eventbus.event.DownloadProgressEvent; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; + +import com.google.common.eventbus.EventBus; + +/** + * An service which combines the functionality for functional use cases to write + * into the cache an notify the writing to the cache to the {@link EventBus}. + * + * + * + * + */ +@Service +public class CacheWriteNotify { + + /** + * + */ + private static final int DOWNLOAD_PROGRESS_MAX = 100; + + @Autowired + private CacheManager cacheManager; + + @Autowired + private EventBus eventBus; + + @Autowired + private TenantAware tenantAware; + + /** + * writes the download progress in percentage into the cache + * {@link CacheKeys#DOWNLOAD_PROGRESS_PERCENT} and notifies the + * {@link EventBus} with a {@link DownloadProgressEvent}. + * + * @param statusId + * the ID of the {@link ActionStatus} + * @param progressPercent + * the progress in percentage which must be between 0-100 + */ + public void downloadProgressPercent(final long statusId, final int progressPercent) { + + final Cache cache = cacheManager.getCache(Action.class.getName()); + final String cacheKey = CacheKeys.entitySpecificCacheKey(String.valueOf(statusId), + CacheKeys.DOWNLOAD_PROGRESS_PERCENT); + if (progressPercent < DOWNLOAD_PROGRESS_MAX) { + cache.put(cacheKey, progressPercent); + } else { + // in case we reached progress 100 delete the cache value again + // because otherwise he will + // keep there forever + cache.evict(cacheKey); + } + + eventBus.post(new DownloadProgressEvent(tenantAware.getCurrentTenant(), statusId, progressPercent)); + } + + /** + * @param cacheManager + * the cacheManager to set + */ + void setCacheManager(final CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + /** + * @param eventBus + * the eventBus to set + */ + void setEventBus(final EventBus eventBus) { + this.eventBus = eventBus; + } + + /** + * @param tenantAware + * the tenantAware to set + */ + void setTenantAware(final TenantAware tenantAware) { + this.tenantAware = tenantAware; + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/ArtifactStoreController.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/ArtifactStoreController.java new file mode 100755 index 000000000..7d1f402ae --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/ArtifactStoreController.java @@ -0,0 +1,200 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller; + +import java.io.IOException; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; +import org.eclipse.hawkbit.cache.CacheWriteNotify; +import org.eclipse.hawkbit.repository.ArtifactManagement; +import org.eclipse.hawkbit.repository.ControllerManagement; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.Artifact; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.rest.resource.helper.RestResourceConversionHelper; +import org.eclipse.hawkbit.util.IpUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.web.bind.annotation.AuthenticationPrincipal; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * The {@link ArtifactStoreController} of the SP server controller API that is + * queried by the SP target in order to download artifacts independent of their + * own individual resource. This is offered in addition to the + * {@link RootController#downloadArtifact(String, Long, Long, javax.servlet.http.HttpServletResponse)} + * for legacy controllers that can not be fed with a download URI at runtime. + * + * + * + * + * + */ +@RestController +@Transactional +@RequestMapping(ControllerConstants.ARTIFACTS_V1_REQUEST_MAPPING) +public class ArtifactStoreController implements EnvironmentAware { + private static final Logger LOG = LoggerFactory.getLogger(ArtifactStoreController.class); + + @Autowired + private ArtifactManagement artifactManagement; + + @Autowired + private ControllerManagement controllerManagement; + + @Autowired + private CacheWriteNotify cacheWriteNotify; + + private static final String SP_SERVER_CONFIG_PREFIX = "hawkbit.server."; + private RelaxedPropertyResolver environment; + + @Override + public void setEnvironment(final Environment environment) { + this.environment = new RelaxedPropertyResolver(environment, SP_SERVER_CONFIG_PREFIX); + + } + + /** + * Handles GET {@link Artifact} download request. This could be full or + * partial download request. + * + * @param fileName + * to search for + * @param response + * to write to + * @param request + * from the client + * @param targetid + * of authenticated target + * + * @return response of the servlet which in case of success is status code + * {@link HttpStatus#OK} or in case of partial download + * {@link HttpStatus#PARTIAL_CONTENT}. + */ + @RequestMapping(method = RequestMethod.GET, value = ControllerConstants.ARTIFACT_DOWNLOAD_BY_FILENAME + + "/{fileName}") + @ResponseBody + public ResponseEntity downloadArtifactByFilename(@PathVariable final String fileName, + final HttpServletResponse response, final HttpServletRequest request, + @AuthenticationPrincipal final String targetid) { + ResponseEntity result = null; + + final List foundArtifacts = artifactManagement.findLocalArtifactByFilename(fileName); + + if (foundArtifacts.isEmpty()) { + LOG.warn("Software artifact with name {} could not be found.", fileName); + result = new ResponseEntity<>(HttpStatus.NOT_FOUND); + } else { + if (foundArtifacts.size() > 1) { + LOG.warn("Software artifact name {} is not unique. We will use the first entry.", fileName); + } + + final LocalArtifact artifact = foundArtifacts.get(0); + + final String ifMatch = request.getHeader("If-Match"); + if (ifMatch != null && !RestResourceConversionHelper.matchesHttpHeader(ifMatch, artifact.getSha1Hash())) { + result = new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED); + } else { + final DbArtifact file = artifactManagement.loadLocalArtifactBinary(artifact); + + // we set a download status only if we are aware of the + // targetid, i.e. + // authenticated and not anonymous + if (targetid != null && !"anonymous".equals(targetid)) { + final Action action = checkAndReportDownloadByTarget(request, targetid, artifact); + result = RestResourceConversionHelper.writeFileResponse(artifact, response, request, file, + cacheWriteNotify, action.getId()); + } else { + result = RestResourceConversionHelper.writeFileResponse(artifact, response, request, file); + } + + } + } + + return result; + } + + private Action checkAndReportDownloadByTarget(final HttpServletRequest request, final String targetid, + final LocalArtifact artifact) { + final Target target = controllerManagement.updateLastTargetQuery( + targetid, + IpUtil.getClientIpFromRequest(request, + environment.getProperty("security.rp.remote_ip_header", String.class, "X-Forwarded-For"))); + + final Action action = controllerManagement.getActionForDownloadByTargetAndSoftwareModule( + target.getControllerId(), artifact.getSoftwareModule()); + final String range = request.getHeader("Range"); + + final ActionStatus actionStatus = new ActionStatus(); + actionStatus.setAction(action); + actionStatus.setOccurredAt(System.currentTimeMillis()); + actionStatus.setStatus(Status.DOWNLOAD); + + if (range != null) { + actionStatus.addMessage("It is a partial download request: " + range); + } else { + actionStatus.addMessage("Target downloads"); + } + controllerManagement.addActionStatusMessage(actionStatus); + return action; + } + + /** + * Handles GET {@link Artifact} MD5 checksum file download request. + * + * @param fileName + * to search for + * @param response + * to write to + * + * @return response of the servlet + */ + @RequestMapping(method = RequestMethod.GET, value = ControllerConstants.ARTIFACT_DOWNLOAD_BY_FILENAME + + "/{fileName}" + ControllerConstants.ARTIFACT_MD5_DWNL_SUFFIX) + @ResponseBody + public ResponseEntity downloadArtifactMD5ByFilename(@PathVariable final String fileName, + final HttpServletResponse response) { + final List foundArtifacts = artifactManagement.findLocalArtifactByFilename(fileName); + + if (foundArtifacts.isEmpty()) { + LOG.warn("Softeare artifact with name {} could not be found.", fileName); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } else if (foundArtifacts.size() > 1) { + LOG.error("Softeare artifact name {} is not unique.", fileName); + } + + try { + DataConversionHelper.writeMD5FileResponse(fileName, response, foundArtifacts.get(0)); + } catch (final IOException e) { + LOG.error("Failed to stream MD5 File", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + + return new ResponseEntity<>(HttpStatus.OK); + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/ControllerConstants.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/ControllerConstants.java new file mode 100755 index 000000000..618091e1d --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/ControllerConstants.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller; + +/** + * + * + * + */ +public final class ControllerConstants { + /** + * The base URL mapping of the direct device integration rest resources. + */ + public static final String BASE_V1_REQUEST_MAPPING = "/{tenant}/controller/v1"; + + /** + * The base URL mapping of the artifact repository rest resources. + */ + public static final String ARTIFACTS_V1_REQUEST_MAPPING = "/{tenant}/controller/artifacts/v1"; + + /** + * Deployment action resources. + */ + static final String DEPLOYMENT_BASE_ACTION = "deploymentBase"; + + /** + * Cancel action resources. + */ + static final String CANCEL_ACTION = "cancelAction"; + + /** + * Feedback channel. + */ + static final String FEEDBACK = "feedback"; + + /** + * Config data action resources. + */ + static final String CONFIG_DATA_ACTION = "configData"; + + /** + * The artifact URL mapping rest resource. + */ + static final String ARTIFACT_DOWNLOAD = "artifact"; + + /** + * The artifact by filename URL mapping rest resource. + */ + static final String ARTIFACT_DOWNLOAD_BY_FILENAME = "/filename"; + + /** + * File suffix for MDH hash download (see Linux md5sum). + */ + static final String ARTIFACT_MD5_DWNL_SUFFIX = ".MD5SUM"; + + // constant class, private constructor. + private ControllerConstants() { + + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/DataConversionHelper.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/DataConversionHelper.java new file mode 100755 index 000000000..edf0e7b90 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/DataConversionHelper.java @@ -0,0 +1,184 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.hawkbit.controller.model.Artifact; +import org.eclipse.hawkbit.controller.model.Chunk; +import org.eclipse.hawkbit.controller.model.Config; +import org.eclipse.hawkbit.controller.model.ControllerBase; +import org.eclipse.hawkbit.controller.model.Polling; +import org.eclipse.hawkbit.controller.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.rest.resource.model.artifact.ArtifactHash; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.springframework.hateoas.Link; + +import com.google.common.base.Charsets; + +/** + * Utility class for the Controller API. + * + * + * + * + */ +public final class DataConversionHelper { + + // utility class, private constructor. + private DataConversionHelper() { + + } + + static List createChunks(final String targetid, final Action uAction, final TenantAware tenantAware) { + return uAction.getDistributionSet() + .getModules().stream().map(module -> new Chunk(mapChunkLegacyKeys(module.getType().getKey()), + module.getVersion(), module.getName(), createArtifacts(targetid, module, tenantAware))) + .collect(Collectors.toList()); + + } + + /** + * Creates all available (rest) software modules for a given distributionSet + * set. + * + * @param targetid + * of the target + * @param assignedDistributionSet + * the assigned distribution set + * @param tenantAware + * of the tenant + * @return a list of software modules or a empty list. Cannot be . + */ + public static List createSoftwareModules(final String targetid, + final DistributionSet assignedDistributionSet, final TenantAware tenantAware) { + return assignedDistributionSet.getModules().stream() + .map(module -> createSoftwareModul(targetid, module, tenantAware)).collect(Collectors.toList()); + + } + + private static SoftwareModule createSoftwareModul(final String targetid, + final org.eclipse.hawkbit.repository.model.SoftwareModule module, final TenantAware tenantAware) { + final List links = new ArrayList(); + module.getLocalArtifacts().forEach(artifact -> createAndAddLinks(targetid, tenantAware, artifact, links)); + + return new SoftwareModule(module.getId(), module.getType().getKey(), module.getVersion(), module.getName(), + links); + } + + private static String mapChunkLegacyKeys(final String key) { + if ("application".equals(key)) { + return "bApp"; + } + if ("runtime".equals(key)) { + return "jvm"; + } + + return key; + } + + /** + * Creates all (rest) artifacts for a given software module. + * + * @param targetid + * of the target + * @param module + * the software module + * @param tenantAware + * of the tenant + * @return a list of artifacts or a empty list. Cannot be . + */ + public static List createArtifacts(final String targetid, + final org.eclipse.hawkbit.repository.model.SoftwareModule module, final TenantAware tenantAware) { + final List files = new ArrayList(); + module.getLocalArtifacts().forEach(artifact -> { + final Artifact file = new Artifact(); + file.setHashes(new ArtifactHash(artifact.getSha1Hash(), artifact.getMd5Hash())); + file.setFilename(artifact.getFilename()); + file.setSize(artifact.getSize()); + createAndAddLinks(targetid, tenantAware, artifact, file.getLinks()); + files.add(file); + }); + return files; + } + + private static void createAndAddLinks(final String targetid, final TenantAware tenantAware, + final LocalArtifact artifact, final List links) { + links.add(linkTo(methodOn(RootController.class, tenantAware.getCurrentTenant()).downloadArtifact(targetid, + artifact.getSoftwareModule().getId(), artifact.getFilename(), null, null)).withRel("download")); + links.add(linkTo(methodOn(RootController.class, tenantAware.getCurrentTenant()).downloadArtifactMd5(targetid, + artifact.getSoftwareModule().getId(), artifact.getFilename(), null, null)).withRel("md5sum")); + } + + static ControllerBase fromTarget(final Target target, final List actions, + final String defaultControllerPollTime, final TenantAware tenantAware) { + final ControllerBase result = new ControllerBase(new Config(new Polling(defaultControllerPollTime))); + + boolean addedUpdate = false; + boolean addedCancel = false; + final long countCancelingActions = actions.stream().filter(a -> a.getStatus() == Status.CANCELING).count(); + for (final Action action : actions) { + if (countCancelingActions <= 0 && !action.isCancelingOrCanceled() && !addedUpdate) { + // we need to add the hashcode here of the actionWithStatus + // because the action might + // have changed from 'soft' to 'forced' type and we need to + // change the payload of the + // response because of eTags. + result.add(linkTo(methodOn(RootController.class, tenantAware.getCurrentTenant()) + .getControllerBasedeploymentAction(target.getControllerId(), action.getId(), actions.hashCode(), + null)).withRel(ControllerConstants.DEPLOYMENT_BASE_ACTION)); + addedUpdate = true; + } else if (action.isCancelingOrCanceled() && !addedCancel) { + result.add(linkTo(methodOn(RootController.class, tenantAware.getCurrentTenant()) + .getControllerCancelAction(target.getControllerId(), action.getId(), null)) + .withRel(ControllerConstants.CANCEL_ACTION)); + addedCancel = true; + } + } + + if (target.getTargetInfo().isRequestControllerAttributes()) { + result.add(linkTo(methodOn(RootController.class, tenantAware.getCurrentTenant()).putConfigData(null, + target.getControllerId(), null)).withRel(ControllerConstants.CONFIG_DATA_ACTION)); + } + return result; + } + + static void writeMD5FileResponse(final String fileName, final HttpServletResponse response, + final LocalArtifact artifact) throws IOException { + final StringBuilder builder = new StringBuilder(); + builder.append(artifact.getMd5Hash()); + builder.append(" "); + builder.append(fileName); + final byte[] content = builder.toString().getBytes(Charsets.US_ASCII); + + final StringBuilder header = new StringBuilder(); + header.append("attachment;filename="); + header.append(fileName); + header.append(ControllerConstants.ARTIFACT_MD5_DWNL_SUFFIX); + + response.setContentLength(content.length); + response.setHeader("Content-Disposition", header.toString()); + + response.getOutputStream().write(content, 0, content.length); + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/EnableDirectDeviceApi.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/EnableDirectDeviceApi.java new file mode 100755 index 000000000..846e1e8d2 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/EnableDirectDeviceApi.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Controller; + +/** + * Annotation to enable {@link ComponentScan} in the resource package to setup + * all {@link Controller} annotated classes and setup the Direct Device API. + * + * + * + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Configuration +@ComponentScan +public @interface EnableDirectDeviceApi { + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/FileSteamingFailedException.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/FileSteamingFailedException.java new file mode 100755 index 000000000..b4dae3b88 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/FileSteamingFailedException.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * Thrown if artifact content streaming to client failed. + * + * + * + * + */ +public final class FileSteamingFailedException extends SpServerRtException { + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new FileUploadFailedException with + * {@link SpServerError#SP_REST_BODY_NOT_READABLE} error. + */ + public FileSteamingFailedException() { + super(SpServerError.SP_ARTIFACT_LOAD_FAILED); + } + + /** + * @param cause + * for the exception + */ + public FileSteamingFailedException(final Throwable cause) { + super(SpServerError.SP_ARTIFACT_LOAD_FAILED, cause); + } + + /** + * @param message + * of the error + */ + public FileSteamingFailedException(final String message) { + super(message, SpServerError.SP_ARTIFACT_LOAD_FAILED); + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/RootController.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/RootController.java new file mode 100755 index 000000000..c62a46822 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/RootController.java @@ -0,0 +1,664 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; + +import org.eclipse.hawkbit.ControllerPollProperties; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; +import org.eclipse.hawkbit.cache.CacheWriteNotify; +import org.eclipse.hawkbit.controller.model.ActionFeedback; +import org.eclipse.hawkbit.controller.model.Cancel; +import org.eclipse.hawkbit.controller.model.CancelActionToStop; +import org.eclipse.hawkbit.controller.model.Chunk; +import org.eclipse.hawkbit.controller.model.ConfigData; +import org.eclipse.hawkbit.controller.model.ControllerBase; +import org.eclipse.hawkbit.controller.model.Deployment; +import org.eclipse.hawkbit.controller.model.Deployment.HandlingType; +import org.eclipse.hawkbit.controller.model.DeploymentBase; +import org.eclipse.hawkbit.controller.model.Result.FinalResult; +import org.eclipse.hawkbit.repository.ArtifactManagement; +import org.eclipse.hawkbit.repository.ControllerManagement; +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.Artifact; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.eclipse.hawkbit.rest.resource.helper.RestResourceConversionHelper; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.eclipse.hawkbit.util.IpUtil; +import org.hibernate.validator.constraints.NotEmpty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * The {@link RootController} of the SP server controller API that is queried by + * the SP controller in order to pull {@link Action}s that have to be fullfilled + * and report status updates concerning the {@link Action} processing. + * + * Transactional (read-write) as all queries at least update the last poll time. + * + * + * + * + * + */ +@RestController +@RequestMapping(ControllerConstants.BASE_V1_REQUEST_MAPPING) +@Transactional +@Api(value = "/", description = "SP Direct Device Integration API") +public class RootController implements EnvironmentAware { + + private static final Logger LOG = LoggerFactory.getLogger(RootController.class); + private static final String GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET = "given action ({}) is not assigned to given target ({})."; + + private static final String SP_SERVER_CONFIG_PREFIX = "hawkbit.server."; + + @Autowired + private ControllerManagement controllerManagement; + + @Autowired + private SoftwareManagement softwareManagement; + + @Autowired + private ArtifactManagement artifactManagement; + + @Autowired + private ControllerPollProperties controllerPollProperties; + + @Autowired + private CacheWriteNotify cacheWriteNotify; + + @Autowired + private TenantAware tenantAware; + + private String requestHeader; + + @Override + public void setEnvironment(final Environment environment) { + final RelaxedPropertyResolver relaxedPropertyResolver = new RelaxedPropertyResolver(environment, + SP_SERVER_CONFIG_PREFIX); + + requestHeader = relaxedPropertyResolver.getProperty("security.rp.remote_ip_header", String.class, + "X-Forwarded-For"); + } + + /** + * Returns all artifacts of a given software module and target. + * + * @param targetid + * of the {@link Target} that matches to + * {@link Target#getControllerId()} + * @param softwareModuleId + * of the {@link SoftwareModule} + * @return the response + */ + @RequestMapping(method = RequestMethod.GET, value = "/{targetid}/softwaremodules/{softwareModuleId}/artifacts", produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = org.eclipse.hawkbit.controller.model.Artifact.class, value = "Returns artifacts of given software module", notes = "Returns all artifacts whichs is assigned to the software module") + public ResponseEntity> getSoftwareModulesArtifacts( + @PathVariable final String targetid, @PathVariable final Long softwareModuleId) { + LOG.debug("getSoftwareModulesArtifacts({})", targetid); + + final SoftwareModule softwareModule = softwareManagement.findSoftwareModuleById(softwareModuleId); + + if (softwareModule == null) { + LOG.warn("Software module with id {} could not be found.", softwareModuleId); + throw new EntityNotFoundException("Software module does not exist"); + + } + + return new ResponseEntity<>(DataConversionHelper.createArtifacts(targetid, softwareModule, tenantAware), + HttpStatus.OK); + } + + /** + * Returns all available software modules for a given target. + * + * @param targetid + * of the {@link Target} that matches to + * {@link Target#getControllerId()} + * @param request + * the HTTP request injected by spring + * @return the response + */ + @RequestMapping(method = RequestMethod.GET, value = "/{targetid}/softwaremodules/", produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = SoftwareModule.class, value = " Returns software modules of given target", notes = "Returns all available software modules for a given target") + public ResponseEntity> getSoftwareModules( + @PathVariable final String targetid, final HttpServletRequest request) { + LOG.debug("getSoftwareModules({})", targetid); + + final Target target = controllerManagement.updateLastTargetQuery(targetid, + IpUtil.getClientIpFromRequest(request, requestHeader)); + + final DistributionSet assignedDistributionSet = target.getAssignedDistributionSet(); + + if (assignedDistributionSet == null) { + return new ResponseEntity<>(new ArrayList<>(), HttpStatus.OK); + } + + return new ResponseEntity<>(DataConversionHelper.createSoftwareModules(targetid, assignedDistributionSet, + tenantAware), HttpStatus.OK); + } + + /** + * Root resource for an individual {@link Target}. + * + * @param targetid + * of the {@link Target} that matches to + * {@link Target#getControllerId()} + * @param request + * the HTTP request injected by spring + * @return the response + */ + @RequestMapping(method = RequestMethod.GET, value = "/{targetid}", produces = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = ControllerBase.class, value = "Controller base poll resource", notes = "This base resource can be regularly polled by the controller on the provisiong target or device " + + "in order to retrieve actions that need to be executed. The resource supports Etag based modification " + + "checks in order to save traffic.") + public ResponseEntity getControllerBase(@PathVariable final String targetid, + final HttpServletRequest request) { + LOG.debug("getControllerBase({})", targetid); + + final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotexist(targetid, + IpUtil.getClientIpFromRequest(request, requestHeader)); + + if (target.getTargetInfo().getUpdateStatus() == TargetUpdateStatus.UNKNOWN) { + LOG.debug("target with {} extsisted but was in status UNKNOWN -> REGISTERED)", targetid); + controllerManagement.updateTargetStatus(target.getTargetInfo(), TargetUpdateStatus.REGISTERED, + System.currentTimeMillis(), IpUtil.getClientIpFromRequest(request, requestHeader)); + } + + return new ResponseEntity(DataConversionHelper.fromTarget(target, + controllerManagement.findActionByTargetAndActive(target), controllerPollProperties.getPollingTime(), + tenantAware), HttpStatus.OK); + } + + /** + * Handles GET {@link Artifact} download request. This could be full or + * partial (as specified by RFC7233 (Range Requests)) download request. + * + * @param targetid + * of the related + * @param softwareModuleId + * of the parent {@link SoftwareModule} + * @param fileName + * of the related {@link LocalArtifact} + * @param response + * of the servlet + * @param request + * from the client + * + * @return response of the servlet which in case of success is status code + * {@link HttpStatus#OK} or in case of partial download + * {@link HttpStatus#PARTIAL_CONTENT}. + */ + @RequestMapping(method = RequestMethod.GET, value = "/{targetid}/softwaremodules/{softwareModuleId}/artifacts/{fileName}") + @ApiOperation(response = Void.class, value = "Downstream of given artifact", notes = "Download resource for artifacts. The resource supports partial download " + + "as specified by RFC7233 (range requests). Keep in mind that the controller " + + "needs to have the artifact assigned in order to be granted permission to download.") + public ResponseEntity downloadArtifact(@PathVariable final String targetid, + @PathVariable final Long softwareModuleId, @PathVariable final String fileName, + final HttpServletResponse response, final HttpServletRequest request) { + ResponseEntity result = null; + + final Target target = controllerManagement.updateLastTargetQuery(targetid, + IpUtil.getClientIpFromRequest(request, requestHeader)); + final SoftwareModule module = softwareManagement.findSoftwareModuleById(softwareModuleId); + + if (checkModule(fileName, module)) { + LOG.warn("Softare module with id {} could not be found.", softwareModuleId); + result = new ResponseEntity<>(HttpStatus.NOT_FOUND); + } else { + final LocalArtifact artifact = module.getLocalArtifactByFilename(fileName).get(); + final DbArtifact file = artifactManagement.loadLocalArtifactBinary(artifact); + + final String ifMatch = request.getHeader("If-Match"); + if (ifMatch != null && !RestResourceConversionHelper.matchesHttpHeader(ifMatch, artifact.getSha1Hash())) { + result = new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED); + } else { + final Action action = checkAndLogDownload(request, target, module); + result = RestResourceConversionHelper.writeFileResponse(artifact, response, request, file, + cacheWriteNotify, action.getId()); + } + + } + + return result; + } + + private Action checkAndLogDownload(final HttpServletRequest request, final Target target, + final SoftwareModule module) { + final Action action = controllerManagement.getActionForDownloadByTargetAndSoftwareModule( + target.getControllerId(), module); + final String range = request.getHeader("Range"); + + final ActionStatus statusMessage = new ActionStatus(); + statusMessage.setAction(action); + statusMessage.setOccurredAt(System.currentTimeMillis()); + statusMessage.setStatus(Status.DOWNLOAD); + + if (range != null) { + statusMessage.addMessage("It is a partial download request: " + range); + } else { + statusMessage.addMessage("Controller downloads"); + } + controllerManagement.addActionStatusMessage(statusMessage); + return action; + } + + private boolean checkModule(final String fileName, final SoftwareModule module) { + return null == module || !module.getLocalArtifactByFilename(fileName).isPresent(); + } + + /** + * Handles GET {@link Artifact} MD5 checksum file download request. + * + * @param targetid + * of the related + * @param softwareModuleId + * of the parent {@link SoftwareModule} + * @param fileName + * of the related {@link LocalArtifact} + * @param response + * of the servlet + * @param request + * the HTTP request injected by spring + * + * @return {@link ResponseEntity} with status {@link HttpStatus#OK} if + * successful + */ + @RequestMapping(method = RequestMethod.GET, value = "/{targetid}/softwaremodules/{softwareModuleId}/artifacts/{fileName}" + + ControllerConstants.ARTIFACT_MD5_DWNL_SUFFIX, produces = MediaType.TEXT_PLAIN_VALUE) + @ApiOperation(response = Void.class, value = "Downstream of given artifacts MD5SUM file", notes = "Download resource for MD5SUM file is an optional functionally especially usefull for " + + "Linux based devices on order to check artifact coonsitency after download by using the md5sum " + + "command line tool. The MD5 and SHA1 are in addition available as metadata in the deployment command itself.") + public ResponseEntity downloadArtifactMd5(@PathVariable final String targetid, + @PathVariable final Long softwareModuleId, @PathVariable final String fileName, + final HttpServletResponse response, final HttpServletRequest request) { + controllerManagement.updateLastTargetQuery(targetid, IpUtil.getClientIpFromRequest(request, requestHeader)); + + final SoftwareModule module = softwareManagement.findSoftwareModuleById(softwareModuleId); + + if (checkModule(fileName, module)) { + LOG.warn("Software module with id {} could not be found.", softwareModuleId); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + try { + DataConversionHelper.writeMD5FileResponse(fileName, response, module.getLocalArtifactByFilename(fileName) + .get()); + } catch (final IOException e) { + LOG.error("Failed to stream MD5 File", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + + return new ResponseEntity<>(HttpStatus.OK); + } + + /** + * Resource for {@link SoftwareModule} {@link UpdateAction}s. + * + * @param targetid + * of the {@link Target} that matches to + * {@link Target#getControllerId()} + * @param actionId + * of the {@link DeploymentBase} that matches to + * {@link Target#getActiveActions()} + * @param resource + * an hashcode of the resource which indicates if the action has + * been changed, e.g. from 'soft' to 'force' and the eTag needs + * to be re-generated + * @param request + * the HTTP request injected by spring + * @return the response + */ + @RequestMapping(value = "/{targetid}/" + ControllerConstants.DEPLOYMENT_BASE_ACTION + "/{actionId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(response = DeploymentBase.class, value = "Deployment or update action", notes = "Core resource for deployment operations. Contains all information necessary in order to execute the operation.") + public ResponseEntity getControllerBasedeploymentAction( + @PathVariable @NotEmpty final String targetid, @PathVariable @NotEmpty final Long actionId, + @RequestParam(value = "c", required = false, defaultValue = "-1") final int resource, + final HttpServletRequest request) { + LOG.debug("getControllerBasedeploymentAction({},{})", targetid, resource); + + final Target target = controllerManagement.updateLastTargetQuery(targetid, + IpUtil.getClientIpFromRequest(request, requestHeader)); + + final Action action = findActionWithExceptionIfNotFound(actionId); + if (!action.getTarget().getId().equals(target.getId())) { + LOG.warn(GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET, action.getId(), target.getId()); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + if (!action.isCancelingOrCanceled()) { + + final List chunks = DataConversionHelper.createChunks(targetid, action, tenantAware); + + final HandlingType handlingType = action.isForce() ? HandlingType.FORCED : HandlingType.ATTEMPT; + + final DeploymentBase base = new DeploymentBase(Long.toString(action.getId()), new Deployment(handlingType, + handlingType, chunks)); + + LOG.debug("Found an active UpdateAction for target {}. returning deyploment: {}", targetid, base); + + controllerManagement.registerRetrieved(action, + "Controller retrieved update action and should start now the download."); + + return new ResponseEntity(base, HttpStatus.OK); + } + + return new ResponseEntity(HttpStatus.NOT_FOUND); + } + + /** + * This is the feedback channel for the {@link DeploymentBase} action. + * + * @param feedback + * to provide + * @param targetid + * of the {@link Target} that matches to + * {@link Target#getControllerId()} + * @param actionId + * of the action we have feedback for + * @param request + * the HTTP request injected by spring + * + * @return the response + */ + @RequestMapping(value = "/{targetid}/" + ControllerConstants.DEPLOYMENT_BASE_ACTION + "/{actionId}/" + + ControllerConstants.FEEDBACK, method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(response = ActionFeedback.class, value = "Feedback channel for update actions", notes = "Feedback channel. It is up to the device to decided how much intermediate feedback is " + + "provided. However, the action will be kept open until the controller on the device reports a " + + "finished (either successfull or error).") + public ResponseEntity postBasedeploymentActionFeedback( + @Valid @RequestBody final ActionFeedback feedback, @PathVariable final String targetid, + @PathVariable @NotEmpty final Long actionId, final HttpServletRequest request) { + LOG.debug("provideBasedeploymentActionFeedback for target [{},{}]: {}", targetid, actionId, feedback); + + final Target target = controllerManagement.updateLastTargetQuery(targetid, + IpUtil.getClientIpFromRequest(request, requestHeader)); + + if (!actionId.equals(feedback.getId())) { + LOG.warn( + "provideBasedeploymentActionFeedback: action in payload ({}) was not identical to action in path ({}).", + feedback.getId(), actionId); + return new ResponseEntity(HttpStatus.NOT_FOUND); + } + + final Action action = findActionWithExceptionIfNotFound(actionId); + if (!action.getTarget().getId().equals(target.getId())) { + LOG.warn(GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET, action.getId(), target.getId()); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + if (!action.isActive()) { + LOG.warn("Updating action {} with feedback {} not possible since action not active anymore.", + action.getId(), feedback.getId()); + return new ResponseEntity(HttpStatus.GONE); + } + + controllerManagement.addUpdateActionStatus(generateUpdateStatus(feedback, targetid, feedback.getId(), action), + action); + + return new ResponseEntity(HttpStatus.OK); + + } + + private ActionStatus generateUpdateStatus(final ActionFeedback feedback, final String targetid, + final Long actionid, final Action action) { + + final ActionStatus actionStatus = new ActionStatus(); + actionStatus.setAction(action); + actionStatus.setOccurredAt(System.currentTimeMillis()); + + switch (feedback.getStatus().getExecution()) { + case CANCELED: + LOG.debug("Controller confirmed cancel (actionid: {}, targetid: {}) as we got {} report.", actionid, + targetid, feedback.getStatus().getExecution()); + actionStatus.setStatus(Status.CANCELED); + actionStatus.addMessage("Controller confirmed cancelation"); + break; + case REJECTED: + LOG.info("Controller reported internal error (actionid: {}, targetid: {}) as we got {} report.", actionid, + targetid, feedback.getStatus().getExecution()); + actionStatus.setStatus(Status.WARNING); + actionStatus.addMessage("Controller reported internal ERROR and REJECTED update."); + break; + case CLOSED: + LOG.debug("Controller reported closed (actionid: {}, targetid: {}) as we got {} report.", actionid, + targetid, feedback.getStatus().getExecution()); + if (feedback.getStatus().getResult().getFinished() == FinalResult.FAILURE) { + actionStatus.setStatus(Status.ERROR); + actionStatus.addMessage("Controller reported CLOSED with ERROR!"); + } else { + actionStatus.setStatus(Status.FINISHED); + actionStatus.addMessage("Controller reported CLOSED with OK!"); + } + break; + default: + LOG.debug("Controller reported intermediate status (actionid: {}, targetid: {}) as we got {} report.", + actionid, targetid, feedback.getStatus().getExecution()); + actionStatus.setStatus(Status.RUNNING); + // MECS-400: we should not use the unstructed message list for + // the + // server comment on the + // status. + actionStatus.addMessage("Controller reported: " + feedback.getStatus().getExecution()); + break; + } + + action.setStatus(actionStatus.getStatus()); + + if (feedback.getStatus().getDetails() != null && !feedback.getStatus().getDetails().isEmpty()) { + final List details = feedback.getStatus().getDetails(); + for (final String detailMsg : details) { + actionStatus.addMessage(detailMsg); + } + } + + return actionStatus; + } + + /** + * This is the feedback channel for the config data action. + * + * @param configData + * as body + * @param targetid + * to provide data for + * @param request + * the HTTP request injected by spring + * + * @return status of the request + */ + @RequestMapping(value = "/{targetid}/" + ControllerConstants.CONFIG_DATA_ACTION, method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(response = ConfigData.class, value = "Response to a requested metadata pull from the provisioning target device.", notes = "The usual behaviour is that when a new device resgisters at the server it is " + + "requested to provide the meta information that will allow the server to identify the device on a " + + "hardware level (e.g. hardware revision, mac address, serial number etc.).") + public ResponseEntity putConfigData(@Valid @RequestBody final ConfigData configData, + @PathVariable final String targetid, final HttpServletRequest request) { + controllerManagement.updateLastTargetQuery(targetid, IpUtil.getClientIpFromRequest(request, requestHeader)); + + controllerManagement.updateControllerAttributes(targetid, configData.getData()); + + return new ResponseEntity(HttpStatus.OK); + } + + /** + * {@link RequestMethod.GET} method for the {@link Cancel} action. + * + * @param targetid + * ID of the calling target + * @param actionId + * of the action + * @param request + * the HTTP request injected by spring + * + * @return the {@link Cancel} response + */ + @RequestMapping(value = "/{targetid}/" + ControllerConstants.CANCEL_ACTION + "/{actionId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(response = Cancel.class, value = "Cancel an action", notes = "The SP server might cancel an operation, e.g. an unfinished update has a sucessor. " + + "It is up to the provisiong target to decide to accept the cancelation or reject it.") + public ResponseEntity getControllerCancelAction(@PathVariable @NotEmpty final String targetid, + @PathVariable @NotEmpty final Long actionId, final HttpServletRequest request) { + LOG.debug("getControllerCancelAction({})", targetid); + + final Target target = controllerManagement.updateLastTargetQuery(targetid, + IpUtil.getClientIpFromRequest(request, requestHeader)); + + final Action action = findActionWithExceptionIfNotFound(actionId); + if (!action.getTarget().getId().equals(target.getId())) { + LOG.warn(GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET, action.getId(), target.getId()); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + if (action.isCancelingOrCanceled()) { + final Cancel cancel = new Cancel(String.valueOf(action.getId()), new CancelActionToStop( + String.valueOf(action.getId()))); + + LOG.debug("Found an active CancelAction for target {}. returning cancel: {}", targetid, cancel); + + controllerManagement.registerRetrieved(action, + "Controller retrieved cancel action and should start now the cancelation."); + + return new ResponseEntity(cancel, HttpStatus.OK); + } + + return new ResponseEntity(HttpStatus.NOT_FOUND); + } + + /** + * {@link RequestMethod.POST} method receiving the {@link ActionFeedback} + * from the target. + * + * @param feedback + * the {@link ActionFeedback} from the target. + * @param targetid + * the ID of the calling target + * @param actionId + * of the action we have feedback for + * @param request + * the HTTP request injected by spring + * + * @return the {@link ActionFeedback} response + */ + @RequestMapping(value = "/{targetid}/" + ControllerConstants.CANCEL_ACTION + "/{actionId}/" + + ControllerConstants.FEEDBACK, method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(response = Cancel.class, value = "Feedback channel for cancel actions", notes = "It is up to the device to decided how much intermediate feedback is " + + "provided. However, the action will be kept open until the controller on the device reports a " + + "finished (either successfull or error) or rejects the oprtioan, e.g. the canceled actions have been started already.") + public ResponseEntity postCancelActionFeedback(@Valid @RequestBody final ActionFeedback feedback, + @PathVariable @NotEmpty final String targetid, @PathVariable @NotEmpty final Long actionId, + final HttpServletRequest request) { + LOG.debug("provideCancelActionFeedback for target [{}]: {}", targetid, feedback); + + final Target target = controllerManagement.updateLastTargetQuery(targetid, + IpUtil.getClientIpFromRequest(request, requestHeader)); + + if (!actionId.equals(feedback.getId())) { + LOG.warn( + "provideBasedeploymentActionFeedback: action in payload ({}) was not identical to action in path ({}).", + feedback.getId(), actionId); + return new ResponseEntity(HttpStatus.NOT_FOUND); + } + + final Action action = findActionWithExceptionIfNotFound(actionId); + if (!action.getTarget().getId().equals(target.getId())) { + LOG.warn(GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET, action.getId(), target.getId()); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + controllerManagement.addCancelActionStatus( + generateActionCancelStatus(feedback, target, feedback.getId(), action), action); + return new ResponseEntity(HttpStatus.OK); + } + + private static ActionStatus generateActionCancelStatus(final ActionFeedback feedback, final Target target, + final Long actionid, final Action action) { + + final ActionStatus actionStatus = new ActionStatus(); + actionStatus.setAction(action); + actionStatus.setOccurredAt(System.currentTimeMillis()); + + switch (feedback.getStatus().getExecution()) { + case CANCELED: + LOG.error( + "Controller reported cancel for a cancel which is not supported by the server (actionid: {}, targetid: {}) as we got {} report.", + actionid, target.getControllerId(), feedback.getStatus().getExecution()); + actionStatus.setStatus(Status.WARNING); + break; + case REJECTED: + LOG.info("Controller rejected the cancelation request (too late) (actionid: {}, targetid: {}).", actionid, + target.getControllerId()); + actionStatus.setStatus(Status.WARNING); + break; + case CLOSED: + if (feedback.getStatus().getResult().getFinished() == FinalResult.FAILURE) { + actionStatus.setStatus(Status.ERROR); + } else { + actionStatus.setStatus(Status.CANCELED); + } + break; + default: + actionStatus.setStatus(Status.RUNNING); + break; + } + + action.setStatus(actionStatus.getStatus()); + + if (feedback.getStatus().getDetails() != null && !feedback.getStatus().getDetails().isEmpty()) { + final List details = feedback.getStatus().getDetails(); + for (final String detailMsg : details) { + actionStatus.addMessage(detailMsg); + } + } + + return actionStatus; + + } + + private Action findActionWithExceptionIfNotFound(final Long actionId) { + final Action findAction = controllerManagement.findActionWithDetails(actionId); + if (findAction == null) { + throw new EntityNotFoundException("Action with Id {" + actionId + "} does not exist"); + } + return findAction; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/ActionFeedback.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/ActionFeedback.java new file mode 100755 index 000000000..0faed14c1 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/ActionFeedback.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller.model; + +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * + *

+ * After the SP Target has executed an action, received by a GET(URL) request it + * reports the completion of it to the SP Server with a action status message, + * i.e. with a PUT message to the feedback channel, i.e. PUT URL/feedback. This + * message could be used not only at the end of execution but also as status + * updates during a longer lasting execution period. The format of each action + * answer message is defined below at each action. But it is expected, that the + * contents of the message answers have all a similar structure: The content + * starts with a generic header and additional elements. * + *

+ * + *

+ * The answer header would look like: { "id": "51659181", "time": + * "20140511T121314", "status": { "execution": "closed", "result": { "final": + * "success", "progress": {} } "details": [], } } + *

+ * + * + * + * + * + * + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@ApiModel("SP Target Action Feedback") +public class ActionFeedback { + + @ApiModelProperty(value = ApiModelProperties.ACTION_ID) + private final Long id; + + @ApiModelProperty(value = ApiModelProperties.TARGET_TIME) + private final String time; + + @ApiModelProperty(value = ApiModelProperties.TARGET_STATUS, required = true) + @NotNull + private final Status status; + + /** + * Constructor. + * + * @param id + * of the actions the feedback is for + * @param time + * of the feedback + * @param status + * is the feedback itself + */ + @JsonCreator + public ActionFeedback(@JsonProperty("id") final Long id, @JsonProperty("time") final String time, + @JsonProperty("status") final Status status) { + this.id = id; + this.time = time; + this.status = status; + } + + public Long getId() { + return id; + } + + public String getTime() { + return time; + } + + public Status getStatus() { + return status; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "ActionFeedback [id=" + id + ", time=" + time + ", status=" + status + "]"; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Artifact.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Artifact.java new file mode 100755 index 000000000..a5efc009d --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Artifact.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller.model; + +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.rest.resource.model.artifact.ArtifactHash; +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; +import org.springframework.hateoas.ResourceSupport; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Download information for all artifacts related to a specific {@link Chunk}. + * + * + * + * + */ +@ApiModel(ApiModelProperties.ARTIFACTS) +public class Artifact extends ResourceSupport { + + @ApiModelProperty(value = ApiModelProperties.ARTIFACT_PROVIDED_FILENAME, required = true) + @NotNull + @JsonProperty + private String filename; + + @ApiModelProperty(value = ApiModelProperties.ARTIFACT_HASHES) + @JsonProperty + private ArtifactHash hashes; + + @ApiModelProperty(value = ApiModelProperties.ARTIFACT_SIZE) + @JsonProperty + private Long size; + + /** + * @return the hashes + */ + public ArtifactHash getHashes() { + return hashes; + } + + /** + * @param hashes + * the hashes to set + */ + public void setHashes(final ArtifactHash hashes) { + this.hashes = hashes; + } + + /** + * @return the fileName + */ + public String getFilename() { + return filename; + } + + /** + * @param fileName + * the fileName to set + */ + public void setFilename(final String fileName) { + filename = fileName; + } + + /** + * @return the size + */ + public Long getSize() { + return size; + } + + /** + * @param size + * the size to set + */ + public void setSize(final Long size) { + this.size = size; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Cancel.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Cancel.java new file mode 100755 index 000000000..0d76f54ae --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Cancel.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller.model; + +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Cancel action to be provided to the target. + * + * + * + */ +@ApiModel("SP Target Cancel Action") +public class Cancel { + @ApiModelProperty(value = ApiModelProperties.ACTION_ID) + private final String id; + + @ApiModelProperty(value = ApiModelProperties.CANCEL_ACTION, required = true) + @NotNull + private final CancelActionToStop cancelAction; + + /** + * Parameterized constructor. + * + * @param id + * of the {@link CancelAction} + * @param cancelAction + * the action + */ + public Cancel(final String id, final CancelActionToStop cancelAction) { + super(); + this.id = id; + this.cancelAction = cancelAction; + } + + /** + * @return the id + */ + public String getId() { + return id; + } + + /** + * @return the cancelAction + */ + public CancelActionToStop getCancelAction() { + return cancelAction; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Cancel [id=" + id + ", cancelAction=" + cancelAction + "]"; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/CancelActionToStop.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/CancelActionToStop.java new file mode 100755 index 000000000..e251aa692 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/CancelActionToStop.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller.model; + +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * The {@link Action} that has to be stopped by the target. + * + * + * + */ +@ApiModel(ApiModelProperties.CANCEL_ACTION) +public class CancelActionToStop { + + @ApiModelProperty(value = ApiModelProperties.ACTION_ID, required = true) + @NotNull + private final String stopId; + + /** + * Parameterized constructor. + * + * @param stopId + * ID of the {@link Action} to be stoppedW + */ + public CancelActionToStop(final String stopId) { + super(); + this.stopId = stopId; + } + + /** + * @return the stopId + */ + public String getStopId() { + return stopId; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "CancelAction [stopId=" + stopId + "]"; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Chunk.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Chunk.java new file mode 100755 index 000000000..8d77da1c4 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Chunk.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller.model; + +import java.util.List; + +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Deployment chunks. + * + * + * + * + * + */ +@ApiModel(ApiModelProperties.CHUNK) +public class Chunk { + + @ApiModelProperty(value = ApiModelProperties.CHUNK_TYPE, required = true) + @NotNull + private final String part; + + @ApiModelProperty(value = ApiModelProperties.CHUNK_VERSION, required = true) + @NotNull + private final String version; + + @ApiModelProperty(value = ApiModelProperties.CHUNK_NAME, required = true) + @NotNull + private final String name; + + @ApiModelProperty(value = ApiModelProperties.ARTIFACTS) + private final List artifacts; + + /** + * Constructor. + * + * @param part + * of the deployment chunk + * @param version + * of the artifact + * @param name + * of the artifact + * @param artifacts + * download information + * + */ + public Chunk(final String part, final String version, final String name, final List artifacts) { + super(); + this.part = part; + this.version = version; + this.name = name; + this.artifacts = artifacts; + } + + public String getPart() { + return part; + } + + public String getVersion() { + return version; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @return the artifacts + */ + public List getArtifacts() { + return artifacts; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Config.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Config.java new file mode 100755 index 000000000..3bfeb95af --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Config.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller.model; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Standard configuration for the target. + * + * + * + * + * + */ +@ApiModel(ApiModelProperties.TARGET_CONFIGURATION) +public class Config { + + @ApiModelProperty(value = ApiModelProperties.TARGET_POLL_TIME) + private final Polling polling; + + /** + * Constructor. + * + * @param polling + * configuration of the SP target + */ + public Config(final Polling polling) { + super(); + this.polling = polling; + } + + public Polling getPolling() { + return polling; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/ConfigData.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/ConfigData.java new file mode 100755 index 000000000..574b3c401 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/ConfigData.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller.model; + +import java.util.Map; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; +import org.hibernate.validator.constraints.NotEmpty; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * feedback channel for ConfigData action. + * + * + * + * + * + */ +@ApiModel("Configuration or metadata that is reponded by the target") +public class ConfigData extends ActionFeedback { + + @ApiModelProperty(ApiModelProperties.TARGET_CONFIG_DATA) + @NotEmpty + private final Map data; + + /** + * Constructor. + * + * @param id + * of the actions the feedback is for + * @param time + * of the feedback + * @param status + * is the feedback itself + * @param data + * contains the attributes. + * + */ + @JsonCreator + public ConfigData(@JsonProperty(value = "id") final Long id, @JsonProperty(value = "time") final String time, + @JsonProperty(value = "status") final Status status, + @JsonProperty(value = "data") final Map data) { + super(id, time, status); + this.data = data; + } + + /** + * @return the data + */ + public Map getData() { + return data; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "ConfigData [data=" + data + ", toString()=" + super.toString() + "]"; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/ControllerBase.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/ControllerBase.java new file mode 100755 index 000000000..47bf11482 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/ControllerBase.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller.model; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; +import org.springframework.hateoas.ResourceSupport; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * {@link ControllerBase} ressource content. + * + * + * + * + * + */ +@ApiModel("SP target base poll resource") +public class ControllerBase extends ResourceSupport { + + @ApiModelProperty(ApiModelProperties.TARGET_CONFIGURATION) + private final Config config; + + /** + * Constructor. + * + * @param config + * configuration of the SP target + */ + public ControllerBase(final Config config) { + super(); + this.config = config; + } + + /** + * @return the config + */ + public Config getConfig() { + return config; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Deployment.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Deployment.java new file mode 100755 index 000000000..d7c8b22a7 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Deployment.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller.model; + +import java.util.List; + +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonValue; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Detailed {@link UpdateAction} information. + * + * + * + * + * + */ +@ApiModel(ApiModelProperties.DEPLOYMENT) +public class Deployment { + + @ApiModelProperty(value = ApiModelProperties.HANDLING_DOWNLOAD) + private final HandlingType download; + + @ApiModelProperty(value = ApiModelProperties.HANDLING_UPDATE) + private final HandlingType update; + + @ApiModelProperty(value = ApiModelProperties.CHUNK) + private final List chunks; + + /** + * Constructor. + * + * @param download + * handling type + * @param update + * handling type + * @param chunks + * to handle. + */ + public Deployment(final HandlingType download, final HandlingType update, final List chunks) { + super(); + this.download = download; + this.update = update; + this.chunks = chunks; + } + + public HandlingType getDownload() { + return download; + } + + public HandlingType getUpdate() { + return update; + } + + public List getChunks() { + return chunks; + } + + /** + * The handling type for the update {@link Action}. + * + * + * + * + * + */ + @ApiModel("Handling type for the deployment part") + public enum HandlingType { + /** + * Not necessary for the command. + */ + SKIP("skip"), + + /** + * Try to execute (local applications may intervene by SP control API). + */ + ATTEMPT("attempt"), + + /** + * Execution independent of local intervention attempts. + */ + FORCED("forced"); + + private String name; + + private HandlingType(final String name) { + this.name = name; + } + + /** + * @return the name + */ + @JsonValue + public String getName() { + return name; + } + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Deployment [download=" + download + ", update=" + update + ", chunks=" + chunks + "]"; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/DeploymentBase.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/DeploymentBase.java new file mode 100755 index 000000000..687765c13 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/DeploymentBase.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller.model; + +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; +import org.springframework.hateoas.ResourceSupport; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * {@link UpdateAction} ressource. + * + * + * + * + * + */ +@ApiModel("Deployment or update action") +public class DeploymentBase extends ResourceSupport { + + @ApiModelProperty(value = ApiModelProperties.ITEM_ID, required = false) + @JsonProperty("id") + @NotNull + private final String deplyomentId; + + @ApiModelProperty(value = ApiModelProperties.DEPLOYMENT, required = false) + @NotNull + private final Deployment deployment; + + /** + * Constructor. + * + * @param id + * of the {@link UpdateAction} + * @param deployment + * details. + */ + public DeploymentBase(final String id, final Deployment deployment) { + deplyomentId = id; + this.deployment = deployment; + } + + public Deployment getDeployment() { + return deployment; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "DeploymentBase [id=" + deplyomentId + ", deployment=" + deployment + "]"; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Polling.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Polling.java new file mode 100755 index 000000000..a7248659d --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Polling.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller.model; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Polling interval for the SP target. + * + * + * + * + * + */ +@ApiModel(value = ApiModelProperties.TARGET_POLL_TIME) +public class Polling { + + @ApiModelProperty(value = ApiModelProperties.TARGET_SLEEP) + private final String sleep; + + /** + * Constructor. + * + * @param sleep + * between polls + */ + public Polling(final String sleep) { + super(); + this.sleep = sleep; + } + + public String getSleep() { + return sleep; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Progress.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Progress.java new file mode 100755 index 000000000..cd0f067b5 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Progress.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller.model; + +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Action fulfillment progress by means of gives the achieved amount of maximal + * of possible levels. + * + * + * + * + * + */ +@ApiModel(ApiModelProperties.TARGET_RESULT_PROGRESS) +public class Progress { + + @ApiModelProperty(value = ApiModelProperties.TARGET_PROGRESS_CNT, required = true) + @NotNull + private final Integer cnt; + + @ApiModelProperty(value = ApiModelProperties.TARGET_PROGRESS_OF) + private final Integer of; + + /** + * Constructor. + * + * @param cnt + * achieved amount + * @param of + * maximum levels + */ + @JsonCreator + public Progress(@JsonProperty("cnt") final Integer cnt, @JsonProperty("of") final Integer of) { + super(); + this.cnt = cnt; + this.of = of; + } + + public Integer getCnt() { + return cnt; + } + + public Integer getOf() { + return of; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Progress [cnt=" + cnt + ", of=" + of + "]"; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Result.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Result.java new file mode 100755 index 000000000..f84da7a25 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Result.java @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller.model; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; +import org.hibernate.validator.constraints.NotEmpty; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Result information of the action progress which can by an intermediate or + * final update. + * + * + * + * + * + */ +@ApiModel(ApiModelProperties.TARGET_RESULT_VALUE) +public class Result { + + @ApiModelProperty(value = ApiModelProperties.TARGET_RESULT_FINISHED, required = true) + @NotEmpty + private final FinalResult finished; + + @ApiModelProperty(value = ApiModelProperties.TARGET_RESULT_PROGRESS) + private final Progress progress; + + /** + * Constructor. + * + * @param finished + * as final result + * @param progress + * if not yet finished + */ + @JsonCreator + public Result(@JsonProperty("finished") final FinalResult finished, + @JsonProperty("progress") final Progress progress) { + super(); + this.finished = finished; + this.progress = progress; + } + + public FinalResult getFinished() { + return finished; + } + + public Progress getProgress() { + return progress; + } + + /** + * Defined status of the final result. + * + * + * + * + * + */ + @ApiModel(value = ApiModelProperties.TARGET_RESULT_FINISHED) + public enum FinalResult { + /** + * Execution was successful. + */ + SUCESS("success"), + + /** + * Execution terminated with errors or without the expected result. + */ + FAILURE("failure"), + + /** + * No final result could be determined (yet). + */ + NONE("none"); + + private String name; + + private FinalResult(final String name) { + this.name = name; + } + + /** + * @return the name + */ + @JsonValue + public String getName() { + return name; + } + + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Result [finished=" + finished + ", progress=" + progress + "]"; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/SoftwareModule.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/SoftwareModule.java new file mode 100755 index 000000000..78e076b87 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/SoftwareModule.java @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller.model; + +import java.util.List; + +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; +import org.springframework.hateoas.Link; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * + * + * + */ +@ApiModel(ApiModelProperties.SOFTWARE_MODUL) +public class SoftwareModule { + + @ApiModelProperty(value = ApiModelProperties.SOFTWARE_MODUL_TYPE, required = true) + @NotNull + private final Long id; + + @ApiModelProperty(value = ApiModelProperties.SOFTWARE_MODUL_TYPE, required = true) + @NotNull + private final String type; + + @ApiModelProperty(value = ApiModelProperties.SOFTWARE_MODULE_VERSION, required = true) + @NotNull + private final String version; + + @ApiModelProperty(value = ApiModelProperties.SOFTWARE_MODULE_NAME, required = true) + @NotNull + private final String name; + + @ApiModelProperty(value = ApiModelProperties.SOFTWARE_MODULE_ARTIFACT_LINKS) + private final List artifactLinks; + + /** + * Constructor. + * + * @param id + * of the software module + * @param type + * of the deployment software module + * @param version + * of the software module + * @param name + * of the software module + * @param artifactLinks + * the links to the artifacts of the software module + */ + public SoftwareModule(final Long id, final String type, final String version, final String name, + final List artifactLinks) { + this.id = id; + this.type = type; + this.version = version; + this.name = name; + this.artifactLinks = artifactLinks; + } + + @JsonProperty("links") + public List getArtifactLinks() { + return artifactLinks; + } + + public Long getId() { + return id; + } + + public String getType() { + return type; + } + + public String getVersion() { + return version; + } + + public String getName() { + return name; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Status.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Status.java new file mode 100755 index 000000000..df88ace73 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/controller/model/Status.java @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller.model; + +import java.util.List; + +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.rest.resource.model.doc.ApiModelProperties; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Details status information concerning the action processing. + * + * + * + * + * + */ +@ApiModel(ApiModelProperties.TARGET_STATUS) +public class Status { + + @ApiModelProperty(value = ApiModelProperties.TARGET_EXEC_STATUS, required = true) + @NotNull + private final ExecutionStatus execution; + + @ApiModelProperty(value = ApiModelProperties.TARGET_RESULT_VALUE, required = true) + @NotNull + private final Result result; + + @ApiModelProperty(value = ApiModelProperties.TARGET_RESULT_DETAILS) + private final List details; + + /** + * Constructor. + * + * @param execution + * status + * @param result + * information + * @param details + * as optional addition + */ + @JsonCreator + public Status(@JsonProperty("execution") final ExecutionStatus execution, + @JsonProperty("result") final Result result, @JsonProperty("details") final List details) { + super(); + this.execution = execution; + this.result = result; + this.details = details; + } + + public ExecutionStatus getExecution() { + return execution; + } + + public Result getResult() { + return result; + } + + public List getDetails() { + return details; + } + + /** + * The element status contains information about the execution of the + * operation. + * + * + * + * + * + */ + @ApiModel(ApiModelProperties.TARGET_EXEC_STATUS) + public enum ExecutionStatus { + /** + * Execution of the action has finished. + */ + CLOSED("closed"), + + /** + * Execution has started but has not yet finished. + */ + PROCEEDING("proceeding"), + + /** + * Execution was suspended from outside. + */ + CANCELED("canceled"), + + /** + * Action has been noticed and is intended to run. + */ + SCHEDULED("scheduled"), + + /** + * Action was not accepted. + */ + REJECTED("rejected"), + + /** + * Action is started after a reset, power loss, etc. + */ + RESUMED("resumed"); + + private String name; + + private ExecutionStatus(final String name) { + this.name = name; + } + + /** + * @return the name + */ + @JsonValue + public String getName() { + return name; + } + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Status [execution=" + execution + ", result=" + result + ", details=" + details + "]"; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/DistributionSetMapper.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/DistributionSetMapper.java new file mode 100755 index 000000000..b763e268b --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/DistributionSetMapper.java @@ -0,0 +1,222 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.hawkbit.repository.DistributionSetAssignmentResult; +import org.eclipse.hawkbit.repository.DistributionSetManagement; +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.rest.resource.model.MetadataRest; +import org.eclipse.hawkbit.rest.resource.model.distributionset.DistributionSetRequestBodyCreate; +import org.eclipse.hawkbit.rest.resource.model.distributionset.DistributionSetRest; +import org.eclipse.hawkbit.rest.resource.model.distributionset.DistributionSetsRest; +import org.eclipse.hawkbit.rest.resource.model.distributionset.TargetAssignmentResponseBody; + +/** + * A mapper which maps repository model to RESTful model representation and + * back. + * + * + * + * + */ +final class DistributionSetMapper { + private DistributionSetMapper() { + // Utility class + } + + private static SoftwareModule findSoftwareModuleWithExceptionIfNotFound(final Long softwareModuleId, + final SoftwareManagement softwareManagement) { + final SoftwareModule module = softwareManagement.findSoftwareModuleById(softwareModuleId); + if (module == null) { + throw new EntityNotFoundException("SoftwareModule with Id {" + softwareModuleId + "} does not exist"); + } + + return module; + } + + private static DistributionSetType findDistributionSetTypeWithExceptionIfNotFound( + final String distributionSetTypekey, final DistributionSetManagement distributionSetManagement) { + + final DistributionSetType module = distributionSetManagement + .findDistributionSetTypeByKey(distributionSetTypekey); + if (module == null) { + throw new EntityNotFoundException( + "DistributionSetType with key {" + distributionSetTypekey + "} does not exist"); + } + return module; + } + + /** + * {@link DistributionSetRequestBodyCreate}s to {@link DistributionSet}s. + * + * @param sets + * to convert + * @param softwareManagement + * to use for conversion + * @return converted list of {@link DistributionSet}s + */ + static List dsFromRequest(final Iterable sets, + final SoftwareManagement softwareManagement, final DistributionSetManagement distributionSetManagement) { + + final List mappedList = new ArrayList<>(); + for (final DistributionSetRequestBodyCreate dsRest : sets) { + mappedList.add(fromRequest(dsRest, softwareManagement, distributionSetManagement)); + } + return mappedList; + + } + + /** + * {@link DistributionSetRequestBodyCreate} to {@link DistributionSet}. + * + * @param dsRest + * to convert + * @param softwareManagement + * to use for conversion + * @return converted {@link DistributionSet} + */ + static DistributionSet fromRequest(final DistributionSetRequestBodyCreate dsRest, + final SoftwareManagement softwareManagement, final DistributionSetManagement distributionSetManagement) { + + final DistributionSet result = new DistributionSet(); + result.setDescription(dsRest.getDescription()); + result.setName(dsRest.getName()); + result.setType(findDistributionSetTypeWithExceptionIfNotFound(dsRest.getType(), distributionSetManagement)); + + result.setRequiredMigrationStep(dsRest.isRequiredMigrationStep()); + result.setVersion(dsRest.getVersion()); + + if (dsRest.getOs() != null) { + result.addModule(findSoftwareModuleWithExceptionIfNotFound(dsRest.getOs().getId(), softwareManagement)); + } + + if (dsRest.getApplication() != null) { + result.addModule( + findSoftwareModuleWithExceptionIfNotFound(dsRest.getApplication().getId(), softwareManagement)); + } + + if (dsRest.getRuntime() != null) { + result.addModule( + findSoftwareModuleWithExceptionIfNotFound(dsRest.getRuntime().getId(), softwareManagement)); + } + + if (dsRest.getModules() != null) { + dsRest.getModules().forEach(module -> result + .addModule(findSoftwareModuleWithExceptionIfNotFound(module.getId(), softwareManagement))); + } + + return result; + } + + /** + * From {@link MetadataRest} to {@link DistributionSetMetadata}. + * + * @param ds + * @param metadata + * @return + */ + static List fromRequestDsMetadata(final DistributionSet ds, + final List metadata) { + final List mappedList = new ArrayList<>(metadata.size()); + for (final MetadataRest metadataRest : metadata) { + if (metadataRest.getKey() == null) { + throw new IllegalArgumentException("the key of the metadata must be present"); + } + mappedList.add(new DistributionSetMetadata(metadataRest.getKey(), ds, metadataRest.getValue())); + } + return mappedList; + } + + static DistributionSetRest toResponse(final DistributionSet distributionSet) { + if (distributionSet == null) { + return null; + } + final DistributionSetRest response = new DistributionSetRest(); + RestModelMapper.mapNamedToNamed(response, distributionSet); + + response.setDsId(distributionSet.getId()); + response.setVersion(distributionSet.getVersion()); + response.setComplete(distributionSet.isComplete()); + response.setType(distributionSet.getType().getKey()); + + distributionSet.getModules() + .forEach(module -> response.getModules().add(SoftwareModuleMapper.toResponse(module))); + + response.setRequiredMigrationStep(distributionSet.isRequiredMigrationStep()); + + response.add( + linkTo(methodOn(DistributionSetResource.class).getDistributionSet(response.getDsId())).withRel("self")); + + response.add(linkTo( + methodOn(DistributionSetTypeResource.class).getDistributionSetType(distributionSet.getType().getId())) + .withRel("type")); + + response.add(linkTo(methodOn(DistributionSetResource.class).getMetadata(response.getDsId(), + Integer.parseInt(RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET), + Integer.parseInt(RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT), null, null)) + .withRel("metadata")); + + return response; + } + + static TargetAssignmentResponseBody toResponse(final DistributionSetAssignmentResult dsAssignmentResult) { + final TargetAssignmentResponseBody result = new TargetAssignmentResponseBody(); + result.setAssigned(dsAssignmentResult.getAssigned()); + result.setAlreadyAssigned(dsAssignmentResult.getAlreadyAssigned()); + result.setTotal(dsAssignmentResult.getTotal()); + return result; + } + + static DistributionSetsRest toResponseDistributionSets(final Iterable sets) { + final DistributionSetsRest response = new DistributionSetsRest(); + for (final DistributionSet set : sets) { + response.add(toResponse(set)); + } + return response; + } + + static MetadataRest toResponseDsMetadata(final DistributionSetMetadata metadata) { + final MetadataRest metadataRest = new MetadataRest(); + metadataRest.setKey(metadata.getId().getKey()); + metadataRest.setValue(metadata.getValue()); + return metadataRest; + } + + static List toResponseDsMetadata(final List metadata) { + + final List mappedList = new ArrayList<>(metadata.size()); + for (final DistributionSetMetadata distributionSetMetadata : metadata) { + mappedList.add(toResponseDsMetadata(distributionSetMetadata)); + } + return mappedList; + } + + static List toResponseFromDsList(final List sets) { + final List mappedList = new ArrayList<>(); + if (sets != null) { + for (final DistributionSet set : sets) { + final DistributionSetRest response = toResponse(set); + + mappedList.add(response); + } + } + return mappedList; + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/DistributionSetResource.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/DistributionSetResource.java new file mode 100755 index 000000000..3f7f28201 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/DistributionSetResource.java @@ -0,0 +1,741 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.persistence.EntityManager; + +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.repository.DeploymentManagement; +import org.eclipse.hawkbit.repository.DistributionSetAssignmentResult; +import org.eclipse.hawkbit.repository.DistributionSetFields; +import org.eclipse.hawkbit.repository.DistributionSetManagement; +import org.eclipse.hawkbit.repository.DistributionSetMetadataFields; +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.repository.TargetFields; +import org.eclipse.hawkbit.repository.TargetManagement; +import org.eclipse.hawkbit.repository.TargetWithActionType; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; +import org.eclipse.hawkbit.repository.model.DsMetadataCompositeKey; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.rsql.RSQLUtility; +import org.eclipse.hawkbit.rest.resource.helper.RestResourceConversionHelper; +import org.eclipse.hawkbit.rest.resource.model.ExceptionInfo; +import org.eclipse.hawkbit.rest.resource.model.MetadataRest; +import org.eclipse.hawkbit.rest.resource.model.MetadataRestPageList; +import org.eclipse.hawkbit.rest.resource.model.distributionset.DistributionSetPagedList; +import org.eclipse.hawkbit.rest.resource.model.distributionset.DistributionSetRequestBodyCreate; +import org.eclipse.hawkbit.rest.resource.model.distributionset.DistributionSetRequestBodyUpdate; +import org.eclipse.hawkbit.rest.resource.model.distributionset.DistributionSetRest; +import org.eclipse.hawkbit.rest.resource.model.distributionset.DistributionSetsRest; +import org.eclipse.hawkbit.rest.resource.model.distributionset.TargetAssignmentRequestBody; +import org.eclipse.hawkbit.rest.resource.model.distributionset.TargetAssignmentResponseBody; +import org.eclipse.hawkbit.rest.resource.model.softwaremodule.SoftwareModuleAssigmentRest; +import org.eclipse.hawkbit.rest.resource.model.softwaremodule.SoftwareModulePagedList; +import org.eclipse.hawkbit.rest.resource.model.softwaremodule.SoftwareModuleRest; +import org.eclipse.hawkbit.rest.resource.model.target.TargetPagedList; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * REST Resource handling for {@link DistributionSet} CRUD operations. + * + * + * + * + */ +@RestController +@Transactional(readOnly = true) +@RequestMapping(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING) +@Api(value = "distributionsets", description = "Distribution Set Management API") +public class DistributionSetResource { + private static final Logger LOG = LoggerFactory.getLogger(DistributionSetResource.class); + + @Autowired + private SoftwareManagement softwareManagement; + + @Autowired + private TargetManagement targetManagement; + + @Autowired + private DeploymentManagement deployManagament; + + @Autowired + private SystemManagement systemManagement; + + @Autowired + private TenantAware currentTenant; + + @Autowired + private DistributionSetManagement distributionSetManagement; + + @Autowired + private EntityManager entityManager; + + /** + * Handles the GET request of retrieving all {@link DistributionSet}s within + * SP. + * + * @param pagingOffsetParam + * the offset of list of sets for pagination, might not be + * present in the rest request then default value will be applied + * @param pagingLimitParam + * the limit of the paged request, might not be present in the + * rest request then default value will be applied + * @param sortParam + * the sorting parameter in the request URL, syntax + * {@code field:direction, field:direction} + * @param rsqlParam + * the search parameter in the request URL, syntax + * {@code q=name==abc} + * @return a list of all set for a defined or default page request with + * status OK. The response is always paged. In any failure the + * JsonResponseExceptionHandler is handling the response. + */ + @RequestMapping(method = RequestMethod.GET, produces = { "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = DistributionSetPagedList.class, value = "Get paged list of Distribution Sets", notes = "Handles the GET request of retrieving all distribution sets within SP. Required Permission: " + + SpPermission.READ_REPOSITORY) + public ResponseEntity getDistributionSets( + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam, + @ApiParam(required = false, value = "FIQL syntax search query" + + "" + + "" + + "" + + "" + + "
version==1.0.0distribution sets with version 1.0.0
version=ge=2.0.0distribution sets with version greater-equal 2.0.0
name=li=%25hotfix%25,description=li=%25hotfix%25distribution sets with either 'hotfix' in name or description ignore case
") @RequestParam(value = RestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam) { + + final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); + final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); + final Sort sorting = PagingUtility.sanitizeDistributionSetSortParam(sortParam); + + final Pageable pageable = new OffsetBasedPageRequest(sanitizedOffsetParam, sanitizedLimitParam, sorting); + final Page findDsPage; + if (rsqlParam != null) { + findDsPage = distributionSetManagement.findDistributionSetsAll( + RSQLUtility.parse(rsqlParam, DistributionSetFields.class, entityManager), pageable, false); + } else { + findDsPage = distributionSetManagement.findDistributionSetsAll(pageable, false, null); + } + + final List rest = DistributionSetMapper.toResponseFromDsList(findDsPage.getContent()); + return new ResponseEntity<>(new DistributionSetPagedList(rest, findDsPage.getTotalElements()), HttpStatus.OK); + } + + /** + * Handles the GET request of retrieving a single {@link DistributionSet} + * within SP. + * + * @param distributionSetId + * the ID of the set to retrieve + * + * @return a single {@link DistributionSet} with status OK. + * + * @throws EntityNotFoundException + * in case no {@link DistributionSet} with the given ID exists. + */ + @RequestMapping(method = RequestMethod.GET, value = "/{distributionSetId}", produces = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = DistributionSetRest.class, value = "Get Distribution Set", notes = "Handles the GET request of retrieving a single distribution set within SP. Required Permission: " + + SpPermission.READ_REPOSITORY) + public ResponseEntity getDistributionSet(@PathVariable final Long distributionSetId) { + final DistributionSet foundDs = findDistributionSetWithExceptionIfNotFound(distributionSetId); + + return new ResponseEntity(DistributionSetMapper.toResponse(foundDs), HttpStatus.OK); + } + + /** + * Handles the POST request of creating new distribution sets within SP. The + * request body must always be a list of sets. The requests is delegating to + * the {@link SoftwareManagement#createDistributionSet(DistributionSet))}. + * + * @param sets + * the {@link DistributionSet}s to be created. + * @return In case all sets could successful created the ResponseEntity with + * status code 201 - Created but without ResponseBody. In any + * failure the JsonResponseExceptionHandler is handling the + * response. + */ + @RequestMapping(method = RequestMethod.POST, consumes = { MediaType.APPLICATION_JSON_VALUE, "application/hal+json" }, produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = DistributionSetsRest.class, value = "Create Distribution Sets", notes = "Handles the POST request of creating new distribution sets within SP. The request body must always be a list of sets. Required Permission: " + + SpPermission.CREATE_REPOSITORY) + @ApiResponses(@ApiResponse(code = 409, message = "Conflict", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity createDistributionSets( + @RequestBody final List sets) { + + LOG.debug("creating {} distribution sets", sets.size()); + // set default Ds type if ds type is null + sets.stream() + .filter(ds -> ds.getType() == null) + .forEach( + ds -> ds.setType(systemManagement.getTenantMetadata(currentTenant.getCurrentTenant()) + .getDefaultDsType().getKey())); + + final Iterable createdDSets = distributionSetManagement + .createDistributionSets(DistributionSetMapper.dsFromRequest(sets, softwareManagement, + distributionSetManagement)); + + // we flush to ensure that entity is generated and we can return ID etc. + entityManager.flush(); + + LOG.debug("{} distribution sets created, return status {}", sets.size(), HttpStatus.CREATED); + return new ResponseEntity<>(DistributionSetMapper.toResponseDistributionSets(createdDSets), HttpStatus.CREATED); + } + + /** + * Handles the DELETE request for a single {@link DistributionSet} within + * SP. + * + * @param distributionSetId + * the ID of the {@link DistributionSet} to delete + * @return status OK if delete as successful. + * + */ + @RequestMapping(method = RequestMethod.DELETE, value = "/{distributionSetId}") + @ApiOperation(value = "Delete Distribution Set", notes = "Handles the DELETE request for a single Distribution Set within SP. Required Permission: " + + SpPermission.DELETE_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Distribution Set", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity deleteDistributionSet(@PathVariable final Long distributionSetId) { + final DistributionSet set = findDistributionSetWithExceptionIfNotFound(distributionSetId); + + distributionSetManagement.deleteDistributionSet(set); + + return new ResponseEntity<>(HttpStatus.OK); + } + + /** + * Handles the UPDATE request for a single {@link DistributionSet} within + * SP. + * + * @param distributionSetId + * the ID of the {@link DistributionSet} to delete + * @param toUpdate + * with the data that needs updating + * + * @return status OK if update as successful with updated content. + * + */ + @RequestMapping(method = RequestMethod.PUT, value = "/{distributionSetId}", consumes = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_JSON_VALUE, "application/hal+json" }) + @ApiOperation(response = DistributionSetRest.class, value = "Update Distribution Set", notes = "Handles the UPDATE request for a single Distribution Set within SP. Required Permission: " + + SpPermission.UPDATE_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Distribution Set", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity updateDistributionSet(@PathVariable final Long distributionSetId, + @RequestBody final DistributionSetRequestBodyUpdate toUpdate) { + final DistributionSet set = findDistributionSetWithExceptionIfNotFound(distributionSetId); + + if (toUpdate.getDescription() != null) { + set.setDescription(toUpdate.getDescription()); + } + + if (toUpdate.getName() != null) { + set.setName(toUpdate.getName()); + } + + if (toUpdate.getVersion() != null) { + set.setVersion(toUpdate.getVersion()); + } + + // we flush to ensure that entity is generated and we can return ID etc. + entityManager.flush(); + + return new ResponseEntity(DistributionSetMapper.toResponse(distributionSetManagement + .updateDistributionSet(set)), HttpStatus.OK); + } + + /** + * Handles the GET request of retrieving assigned targets to a specific + * distribution set. + * + * @param distributionSetId + * the ID of the distribution set to retrieve the assigned + * targets + * @param pagingOffsetParam + * the offset of list of targets for pagination, might not be + * present in the rest request then default value will be applied + * @param pagingLimitParam + * the limit of the paged request, might not be present in the + * rest request then default value will be applied + * @param sortParam + * the sorting parameter in the request URL, syntax + * {@code field:direction, field:direction} + * @param rsqlParam + * the search parameter in the request URL, syntax + * {@code q=name==abc} + * @return status OK if get request is successful with the paged list of + * targets + */ + @RequestMapping(method = RequestMethod.GET, value = "/{distributionSetId}/assignedTargets", produces = { + MediaType.APPLICATION_JSON_VALUE, "application/hal+json" }) + @ApiOperation(response = TargetPagedList.class, value = "Get assigned targets", notes = "Handles the GET request for retrieving assigned targets of a single distribution set. Required Permission: " + + SpPermission.READ_REPOSITORY + " and " + SpPermission.READ_TARGET) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Distribution Set", response = ExceptionInfo.class)) + public ResponseEntity getAssignedTargets( + @PathVariable final Long distributionSetId, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam, + @ApiParam(required = false, value = "FIQL syntax search query" + "" + + "" + + "" + + "
controllerId==0815targets with the id '0815'
name=li=%25ccu%25targets which contains 'ccu' in their ignore case
") @RequestParam(value = RestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam) { + + // check if distribution set exists otherwise throw exception + // immediately + findDistributionSetWithExceptionIfNotFound(distributionSetId); + + final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); + final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); + final Sort sorting = PagingUtility.sanitizeTargetSortParam(sortParam); + + final Pageable pageable = new OffsetBasedPageRequest(sanitizedOffsetParam, sanitizedLimitParam, sorting); + final Page targetsAssignedDS; + if (rsqlParam != null) { + targetsAssignedDS = targetManagement.findTargetByAssignedDistributionSet(distributionSetId, + RSQLUtility.parse(rsqlParam, TargetFields.class, entityManager), pageable); + } else { + targetsAssignedDS = targetManagement.findTargetByAssignedDistributionSet(distributionSetId, pageable); + } + + return new ResponseEntity<>(new TargetPagedList(TargetMapper.toResponse(targetsAssignedDS.getContent()), + targetsAssignedDS.getTotalElements()), HttpStatus.OK); + } + + /** + * Handles the GET request of retrieving installed targets to a specific + * distribution set. + * + * @param distributionSetId + * the ID of the distribution set to retrieve the assigned + * targets + * @param pagingOffsetParam + * the offset of list of targets for pagination, might not be + * present in the rest request then default value will be applied + * @param pagingLimitParam + * the limit of the paged request, might not be present in the + * rest request then default value will be applied + * @param sortParam + * the sorting parameter in the request URL, syntax + * {@code field:direction, field:direction} + * @param rsqlParam + * the search parameter in the request URL, syntax + * {@code q=name==abc} + * @return status OK if get request is successful with the paged list of + * targets + */ + @RequestMapping(method = RequestMethod.GET, value = "/{distributionSetId}/installedTargets", produces = { + MediaType.APPLICATION_JSON_VALUE, "application/hal+json" }) + @ApiOperation(response = TargetPagedList.class, value = "Get installed targets", notes = "Handles the GET request for retrieving installed targets of a single distribution set. Required Permission: " + + SpPermission.READ_REPOSITORY + " and " + SpPermission.READ_TARGET) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Distribution Set", response = ExceptionInfo.class)) + public ResponseEntity getInstalledTargets( + @PathVariable final Long distributionSetId, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam, + @ApiParam(required = false, value = "FIQL syntax search query" + "" + + "" + + "" + + "
controllerId==0815targets with the id '0815'
name=li=%25ccu%25targets which contains 'ccu' in their ignore case
") @RequestParam(value = RestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam) { + // check if distribution set exists otherwise throw exception + // immediately + findDistributionSetWithExceptionIfNotFound(distributionSetId); + + final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); + final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); + final Sort sorting = PagingUtility.sanitizeTargetSortParam(sortParam); + + final Pageable pageable = new OffsetBasedPageRequest(sanitizedOffsetParam, sanitizedLimitParam, sorting); + final Page targetsInstalledDS; + if (rsqlParam != null) { + targetsInstalledDS = targetManagement.findTargetByInstalledDistributionSet(distributionSetId, + RSQLUtility.parse(rsqlParam, TargetFields.class, entityManager), pageable); + } else { + targetsInstalledDS = targetManagement.findTargetByInstalledDistributionSet(distributionSetId, pageable); + } + + return new ResponseEntity<>(new TargetPagedList(TargetMapper.toResponse(targetsInstalledDS.getContent()), + targetsInstalledDS.getTotalElements()), HttpStatus.OK); + } + + /** + * Handles the POST request of assigning multiple targets to a single + * distribution set. + * + * @param distributionSetId + * the ID of the distribution set within the URL path parameter + * @param targetIds + * the IDs of the target which should get assigned to the + * distribution set given in the response body + * @return status OK if the assignment of the targets was successful and a + * complex return body which contains information about the assigned + * targets and the already assigned targets counters + */ + @RequestMapping(method = RequestMethod.POST, value = "/{distributionSetId}/assignedTargets", consumes = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_JSON_VALUE, + "application/hal+json" }) + @ApiOperation(response = TargetAssignmentResponseBody.class, value = "Assign targets to a distribution set", notes = "Handles the POST request for assigning multiple targets to a distribution set.The request body must always be a list of target IDs." + + " Required Permission: " + SpPermission.READ_REPOSITORY + " and " + SpPermission.UPDATE_TARGET) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Distribution Set", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity createAssignedTarget( + @PathVariable final Long distributionSetId, + @RequestBody @ApiParam(value = "List of target IDs", required = true) final List targetIds) { + + final DistributionSetAssignmentResult assignDistributionSet = deployManagament.assignDistributionSet( + distributionSetId, + targetIds + .stream() + .map(t -> new TargetWithActionType(t.getId(), RestResourceConversionHelper.convertActionType(t + .getType()), t.getForcetime())).collect(Collectors.toList())); + + // we flush to ensure that entity is generated and we can return ID etc. + entityManager.flush(); + + return new ResponseEntity( + DistributionSetMapper.toResponse(assignDistributionSet), HttpStatus.OK); + } + + /** + * Gets a paged list of meta data for a distribution set. + * + * @param distributionSetId + * the ID of the distribution set for the meta data + * @param pagingOffsetParam + * the offset of list of targets for pagination, might not be + * present in the rest request then default value will be applied + * @param pagingLimitParam + * the limit of the paged request, might not be present in the + * rest request then default value will be applied + * @param sortParam + * the sorting parameter in the request URL, syntax + * {@code field:direction, field:direction} + * @param rsqlParam + * the search parameter in the request URL, syntax + * {@code q=key==abc} + * @return status OK if get request is successful with the paged list of + * meta data + */ + @RequestMapping(method = RequestMethod.GET, value = "/{distributionSetId}/metadata", produces = { + MediaType.APPLICATION_JSON_VALUE, "application/hal+json" }) + @ApiOperation(response = MetadataRestPageList.class, value = "Get a paged list of meta data", notes = " Get a paged list of meta data for a distribution set." + + " Required Permission: " + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Distribution Set", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity getMetadata( + @PathVariable final Long distributionSetId, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam, + @ApiParam(required = false, value = "FIQL syntax search query" + + "" + + "" + + "" + + "
key==aKey,key==bKeymetadata with the key 'aKey' or 'bKey'
value=li=%25someValue%25metadata which contains 'someValue' in the value ignore case
") @RequestParam(value = RestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam) { + + // check if distribution set exists otherwise throw exception + // immediately + findDistributionSetWithExceptionIfNotFound(distributionSetId); + + final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); + final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); + final Sort sorting = PagingUtility.sanitizeDistributionSetMetadataSortParam(sortParam); + + final Pageable pageable = new OffsetBasedPageRequest(sanitizedOffsetParam, sanitizedLimitParam, sorting); + final Page metaDataPage; + + if (rsqlParam != null) { + metaDataPage = distributionSetManagement.findDistributionSetMetadataByDistributionSetId(distributionSetId, + RSQLUtility.parse(rsqlParam, DistributionSetMetadataFields.class, entityManager), pageable); + } else { + metaDataPage = distributionSetManagement.findDistributionSetMetadataByDistributionSetId(distributionSetId, + pageable); + } + + return new ResponseEntity<>(new MetadataRestPageList(DistributionSetMapper.toResponseDsMetadata(metaDataPage + .getContent()), metaDataPage.getTotalElements()), HttpStatus.OK); + + } + + /** + * Gets a single meta data value for a specific key of a distribution set. + * + * @param distributionSetId + * the ID of the distribution set to get the meta data from + * @param metadataKey + * the key of the meta data entry to retrieve the value from + * @return status OK if get request is successful with the value of the meta + * data + */ + @RequestMapping(method = RequestMethod.GET, value = "/{distributionSetId}/metadata/{metadataKey}", produces = { MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = MetadataRest.class, value = "Get a single meta data value", notes = " Get a single meta data value for a meta data key." + + " Required Permission: " + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Distribution Set or meta data key", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity getMetadataValue(@PathVariable final Long distributionSetId, + @PathVariable final String metadataKey) { + // check if distribution set exists otherwise throw exception + // immediately + final DistributionSet ds = findDistributionSetWithExceptionIfNotFound(distributionSetId); + final DistributionSetMetadata findOne = distributionSetManagement.findOne(new DsMetadataCompositeKey(ds, + metadataKey)); + return ResponseEntity. ok(DistributionSetMapper.toResponseDsMetadata(findOne)); + } + + /** + * Updates a single meta data value of a distribution set. + * + * @param distributionSetId + * the ID of the distribution set to update the meta data entry + * @param metadataKey + * the key of the meta data to update the value + * @return status OK if the update request is successful and the updated + * meta data result + */ + @RequestMapping(method = RequestMethod.PUT, value = "/{distributionSetId}/metadata/{metadataKey}", produces = { + MediaType.APPLICATION_JSON_VALUE, "application/hal+json" }) + @ApiOperation(response = MetadataRest.class, value = "Update a single meta data value", notes = " Update a single meta data value for speficic key." + + " Required Permission: " + SpPermission.UPDATE_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Distribution Set or meta data key", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity updateMetadata(@PathVariable final Long distributionSetId, + @PathVariable final String metadataKey, @RequestBody final MetadataRest metadata) { + // check if distribution set exists otherwise throw exception + // immediately + final DistributionSet ds = findDistributionSetWithExceptionIfNotFound(distributionSetId); + final DistributionSetMetadata updated = distributionSetManagement + .updateDistributionSetMetadata(new DistributionSetMetadata(metadataKey, ds, metadata.getValue())); + return ResponseEntity.ok(DistributionSetMapper.toResponseDsMetadata(updated)); + } + + /** + * Deletes a single meta data entry from the distribution set. + * + * @param distributionSetId + * the ID of the distribution set to delete the meta data entry + * @param metadataKey + * the key of the meta data to delete + * @return status OK if the delete request is successful + */ + @RequestMapping(method = RequestMethod.DELETE, value = "/{distributionSetId}/metadata/{metadataKey}") + @ApiOperation(value = "Delete a single meta data", notes = " Delete a single meta data." + " Required Permission: " + + SpPermission.UPDATE_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Distribution Set or meta data key", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity deleteMetadata(@PathVariable final Long distributionSetId, + @PathVariable final String metadataKey) { + // check if distribution set exists otherwise throw exception + // immediately + final DistributionSet ds = findDistributionSetWithExceptionIfNotFound(distributionSetId); + distributionSetManagement.deleteDistributionSetMetadata(new DsMetadataCompositeKey(ds, metadataKey)); + return ResponseEntity.ok().build(); + } + + /** + * Creates a list of meta data for a specific distribution set. + * + * @param distributionSetId + * the ID of the distribution set to create meta data for + * @param metadataRest + * the list of meta data entries to create + * @return status created if post request is successful with the value of + * the created meta data + */ + @RequestMapping(method = RequestMethod.POST, value = "/{distributionSetId}/metadata", consumes = { + MediaType.APPLICATION_JSON_VALUE, "application/hal+json" }, produces = { MediaType.APPLICATION_JSON_VALUE, + "application/hal+json" }) + @ApiOperation(response = MetadataRest.class, value = "Create a list of meta data entries", notes = "Create a list of meta data entries" + + " Required Permission: " + SpPermission.READ_REPOSITORY + " and " + SpPermission.UPDATE_TARGET) + @ApiResponses({ @ApiResponse(code = 409, message = "Conflict", response = ExceptionInfo.class), + @ApiResponse(code = 404, message = "Not Found Distribution Set", response = ExceptionInfo.class) }) + @Transactional + public ResponseEntity> createMetadata(@PathVariable final Long distributionSetId, + @RequestBody final List metadataRest) { + // check if distribution set exists otherwise throw exception + // immediately + final DistributionSet ds = findDistributionSetWithExceptionIfNotFound(distributionSetId); + + final List created = distributionSetManagement + .createDistributionSetMetadata(DistributionSetMapper.fromRequestDsMetadata(ds, metadataRest)); + + // we flush to ensure that entity is generated and we can return ID etc. + entityManager.flush(); + + return new ResponseEntity>(DistributionSetMapper.toResponseDsMetadata(created), + HttpStatus.CREATED); + + } + + /** + * Assigns a list of software modules to a distribution set. + * + * @param distributionSetId + * the ID of the distribution set to assign software modules for + * @param softwareModuleIDs + * the list of software modules ids to assign + * @return {@link HttpStatus#OK} + * + * @throws EntityNotFoundException + * in case no distribution set with the given + * {@code distributionSetId} exists. + */ + @RequestMapping(method = RequestMethod.POST, value = "/{distributionSetId}/assignedSM", consumes = { + MediaType.APPLICATION_JSON_VALUE, "application/hal+json" }, produces = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = SoftwareModuleRest.class, value = "Assign Software Modules to Distribution Set", notes = "Handles the POST request for assigning multiple software modules to a distribution set.The request body must always be a list of software module IDs." + + " Required Permission: " + SpPermission.READ_REPOSITORY + " and " + SpPermission.UPDATE_REPOSITORY) + @ApiResponses({ + @ApiResponse(code = 423, message = "In use of Distribution Set by Target", response = ExceptionInfo.class), + @ApiResponse(code = 404, message = "Not Found Distribution Set key or Software Module IDs", response = ExceptionInfo.class) }) + @Transactional + public ResponseEntity assignSoftwareModules(@PathVariable final Long distributionSetId, + @RequestBody final List softwareModuleIDs) { + // check if distribution set exists otherwise throw exception + // immediately + final DistributionSet ds = findDistributionSetWithExceptionIfNotFound(distributionSetId); + + final Set softwareModuleToBeAssigned = new HashSet(); + for (final SoftwareModuleAssigmentRest sm : softwareModuleIDs) { + final SoftwareModule softwareModule = softwareManagement.findSoftwareModuleById(sm.getId()); + if (softwareModule != null) { + softwareModuleToBeAssigned.add(softwareModule); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + // Add Softwaremodules to DisSet only if all of them were found + distributionSetManagement.assignSoftwareModules(ds, softwareModuleToBeAssigned); + return new ResponseEntity<>(HttpStatus.OK); + } + + /** + * Deletes the assignment of the software module form the distribution set. + * + * @param distributionSetId + * the ID of the distribution set to reject the software module + * for + * @param softwareModuleId + * the software module id to get rejected form the distribution + * set + * @return status OK if rejection was successful. + * @throws EntityNotFoundException + * in case no distribution set with the given + * {@code distributionSetId} exists. + */ + @RequestMapping(method = RequestMethod.DELETE, value = "/{distributionSetId}/assignedSM/{softwareModuleId}") + @ApiOperation(value = "Delete assignment of Software Module", notes = " Delete a assignment." + + " Required Permission: " + SpPermission.UPDATE_REPOSITORY) + @ApiResponses({ + @ApiResponse(code = 423, message = "In use of Distribution Set by Target", response = ExceptionInfo.class), + @ApiResponse(code = 404, message = "Not Found Distribution Set key or Software Module key", response = ExceptionInfo.class) }) + @Transactional + public ResponseEntity deleteAssignSoftwareModules(@PathVariable final Long distributionSetId, + @PathVariable final Long softwareModuleId) { + // check if distribution set and software module exist otherwise throw + // exception immediately + final DistributionSet ds = findDistributionSetWithExceptionIfNotFound(distributionSetId); + final SoftwareModule sm = findSoftwareModuleWithExceptionIfNotFound(softwareModuleId); + distributionSetManagement.unassignSoftwareModule(ds, sm); + return ResponseEntity.ok().build(); + } + + /** + * Handles the GET request for retrieving the assigned software modules of a + * specific distribution set. + * + * @param distributionSetId + * the ID of the distribution to retrieve + * @param pagingOffsetParam + * the offset of list of sets for pagination, might not be + * present in the rest request then default value will be applied + * @param pagingLimitParam + * the limit of the paged request, might not be present in the + * rest request then default value will be applied + * @param sortParam + * the sorting parameter in the request URL, syntax + * {@code field:direction, field:direction} + * @return a list of the assigned software modules of a distribution set + * with status OK, if none is assigned than {@code null} + * @throws EntityNotFoundException + * in case no distribution set with the given + * {@code distributionSetId} exists. + */ + @RequestMapping(method = RequestMethod.GET, value = "/{distributionSetId}/assignedSM", produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = SoftwareModuleRest.class, value = "Get assigned Software Modules", notes = "Handles the GET request of retrieving a single distribution set within SP. Required Permission: " + + SpPermission.READ_REPOSITORY) + public ResponseEntity getAssignedSoftwareModules( + @PathVariable final Long distributionSetId, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam) { + // check if distribution set exists otherwise throw exception + // immediately + final DistributionSet foundDs = findDistributionSetWithExceptionIfNotFound(distributionSetId); + final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); + final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); + final Sort sorting = PagingUtility.sanitizeSoftwareModuleSortParam(sortParam); + final Pageable pageable = new OffsetBasedPageRequest(sanitizedOffsetParam, sanitizedLimitParam, sorting); + final Page softwaremodules = softwareManagement.findSoftwareModuleByAssignedTo(pageable, + foundDs); + return new ResponseEntity<>(new SoftwareModulePagedList(SoftwareModuleMapper.toResponse(softwaremodules + .getContent()), softwaremodules.getTotalElements()), HttpStatus.OK); + } + + private DistributionSet findDistributionSetWithExceptionIfNotFound(final Long distributionSetId) { + final DistributionSet set = distributionSetManagement.findDistributionSetById(distributionSetId); + if (set == null) { + throw new EntityNotFoundException("DistributionSet with Id {" + distributionSetId + "} does not exist"); + } + + return set; + } + + private SoftwareModule findSoftwareModuleWithExceptionIfNotFound(final Long softwareModuleId) { + final SoftwareModule sm = softwareManagement.findSoftwareModuleById(softwareModuleId); + if (sm == null) { + throw new EntityNotFoundException("SoftwareModule with Id {" + softwareModuleId + "} does not exist"); + } + return sm; + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/DistributionSetTypeMapper.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/DistributionSetTypeMapper.java new file mode 100755 index 000000000..0f28c57b5 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/DistributionSetTypeMapper.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.rest.resource.model.distributionsettype.DistributionSetTypeRequestBodyCreate; +import org.eclipse.hawkbit.rest.resource.model.distributionsettype.DistributionSetTypeRest; +import org.eclipse.hawkbit.rest.resource.model.distributionsettype.DistributionSetTypesRest; + +/** + * A mapper which maps repository model to RESTful model representation and + * back. + * + * + * + * + */ +final class DistributionSetTypeMapper { + + // private constructor, utility class + private DistributionSetTypeMapper() { + + } + + static List smFromRequest(final SoftwareManagement softwareManagement, + final Iterable smTypesRest) { + final List mappedList = new ArrayList<>(); + + for (final DistributionSetTypeRequestBodyCreate smRest : smTypesRest) { + mappedList.add(fromRequest(softwareManagement, smRest)); + } + return mappedList; + } + + static DistributionSetType fromRequest(final SoftwareManagement softwareManagement, + final DistributionSetTypeRequestBodyCreate smsRest) { + + final DistributionSetType result = new DistributionSetType(smsRest.getKey(), smsRest.getName(), + smsRest.getDescription()); + + // Add mandatory + smsRest.getMandatorymodules().stream().map(mand -> { + final SoftwareModuleType smType = softwareManagement.findSoftwareModuleTypeById(mand.getId()); + + if (smType == null) { + throw new EntityNotFoundException("SoftwareModuleType with ID " + mand.getId() + " not found"); + } + + return smType; + }).forEach(softmType -> result.addMandatoryModuleType(softmType)); + + // Add optional + smsRest.getOptionalmodules().stream().map(opt -> { + final SoftwareModuleType smType = softwareManagement.findSoftwareModuleTypeById(opt.getId()); + + if (smType == null) { + throw new EntityNotFoundException("SoftwareModuleType with ID " + opt.getId() + " not found"); + } + + return smType; + }).forEach(softmType -> result.addOptionalModuleType(softmType)); + + return result; + } + + static DistributionSetTypesRest toTypesResponse(final List types) { + final DistributionSetTypesRest response = new DistributionSetTypesRest(); + for (final DistributionSetType dsType : types) { + response.add(toResponse(dsType)); + } + return response; + } + + static List toListResponse(final List types) { + final List response = new ArrayList(); + for (final DistributionSetType dsType : types) { + response.add(toResponse(dsType)); + } + return response; + } + + static DistributionSetTypeRest toResponse(final DistributionSetType type) { + final DistributionSetTypeRest result = new DistributionSetTypeRest(); + + RestModelMapper.mapNamedToNamed(result, type); + result.setKey(type.getKey()); + result.setModuleId(type.getId()); + + result.add(linkTo(methodOn(DistributionSetTypeResource.class).getDistributionSetType(result.getModuleId())) + .withRel("self")); + + result.add(linkTo(methodOn(DistributionSetTypeResource.class).getMandatoryModules(result.getModuleId())) + .withRel(RestConstants.DISTRIBUTIONSETTYPE_V1_MANDATORY_MODULES)); + + result.add(linkTo(methodOn(DistributionSetTypeResource.class).getOptionalModules(result.getModuleId())) + .withRel(RestConstants.DISTRIBUTIONSETTYPE_V1_OPTIONAL_MODULES)); + + return result; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/DistributionSetTypeResource.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/DistributionSetTypeResource.java new file mode 100755 index 000000000..ed054abf9 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/DistributionSetTypeResource.java @@ -0,0 +1,495 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; + +import java.util.List; + +import javax.persistence.EntityManager; + +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.repository.DistributionSetManagement; +import org.eclipse.hawkbit.repository.DistributionSetTypeFields; +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.Artifact; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.repository.rsql.RSQLUtility; +import org.eclipse.hawkbit.rest.resource.model.ExceptionInfo; +import org.eclipse.hawkbit.rest.resource.model.IdRest; +import org.eclipse.hawkbit.rest.resource.model.action.ActionPagedList; +import org.eclipse.hawkbit.rest.resource.model.distributionsettype.DistributionSetTypePagedList; +import org.eclipse.hawkbit.rest.resource.model.distributionsettype.DistributionSetTypeRequestBodyCreate; +import org.eclipse.hawkbit.rest.resource.model.distributionsettype.DistributionSetTypeRequestBodyUpdate; +import org.eclipse.hawkbit.rest.resource.model.distributionsettype.DistributionSetTypeRest; +import org.eclipse.hawkbit.rest.resource.model.distributionsettype.DistributionSetTypesRest; +import org.eclipse.hawkbit.rest.resource.model.softwaremoduletype.SoftwareModuleTypeRest; +import org.eclipse.hawkbit.rest.resource.model.softwaremoduletype.SoftwareModuleTypesRest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * REST Resource handling for {@link SoftwareModule} and related + * {@link Artifact} CRUD operations. + * + * + * + * + */ +@RestController +@Transactional(readOnly = true) +@RequestMapping(RestConstants.DISTRIBUTIONSETTYPE_V1_REQUEST_MAPPING) +@Api(value = "distributionsettypes", description = "Distribution Set Types Management API") +public class DistributionSetTypeResource { + + @Autowired + private SoftwareManagement softwareManagement; + + @Autowired + private DistributionSetManagement distributionSetManagement; + + @Autowired + private EntityManager entityManager; + + /** + * Handles the GET request of retrieving all {@link DistributionSetType}s + * within SP. + * + * @param pagingOffsetParam + * the offset of list of modules for pagination, might not be + * present in the rest request then default value will be applied + * @param pagingLimitParam + * the limit of the paged request, might not be present in the + * rest request then default value will be applied + * @param sortParam + * the sorting parameter in the request URL, syntax + * {@code field:direction, field:direction} + * @param rsqlParam + * the search parameter in the request URL, syntax + * {@code q=name==abc} + * + * @return a list of all {@link DistributionSetType} for a defined or + * default page request with status OK. The response is always + * paged. In any failure the JsonResponseExceptionHandler is + * handling the response. + */ + @RequestMapping(method = RequestMethod.GET, produces = { "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = DistributionSetTypePagedList.class, value = "Get distribution det types", notes = "Handles the GET request of retrieving all distribution set types within SP. Required Permission: " + + SpPermission.READ_REPOSITORY) + public ResponseEntity getTypes( + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam, + @ApiParam(required = false, value = "FIQL syntax search query" + + "" + + "" + + "" + + "
id=1,key=vhicletypex2015Distribution Set Types with id 1 or key vhicletypex2015
name=VehicleSeriesADistribution Set Types with name VehicleSeriesA
") @RequestParam(value = RestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam) { + + final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); + final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); + final Sort sorting = PagingUtility.sanitizeSoftwareModuleSortParam(sortParam); + + final Pageable pageable = new OffsetBasedPageRequest(sanitizedOffsetParam, sanitizedLimitParam, sorting); + + final Slice findModuleTypessAll; + Long countModulesAll; + if (rsqlParam != null) { + findModuleTypessAll = distributionSetManagement.findDistributionSetTypesByPredicate( + RSQLUtility.parse(rsqlParam, DistributionSetTypeFields.class, entityManager), pageable); + countModulesAll = ((Page) findModuleTypessAll).getTotalElements(); + } else { + findModuleTypessAll = distributionSetManagement.findDistributionSetTypesAll(pageable); + countModulesAll = distributionSetManagement.countDistributionSetTypesAll(); + } + + final List rest = DistributionSetTypeMapper.toListResponse(findModuleTypessAll + .getContent()); + return new ResponseEntity<>(new DistributionSetTypePagedList(rest, countModulesAll), HttpStatus.OK); + } + + /** + * Handles the GET request of retrieving a single + * {@link DistributionSetType} within SP. + * + * @param distributionSetTypeId + * the ID of the module type to retrieve + * + * @return a single softwareModule with status OK. + * @throws EntityNotFoundException + * in case no with the given {@code softwareModuleId} exists. + */ + @RequestMapping(method = RequestMethod.GET, value = "/{distributionSetTypeId}", produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = DistributionSetTypeRest.class, value = "Get distribution set type", notes = "Handles the GET request of retrieving a single distribution set type within SP. Required Permission: " + + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not found distribution set type", response = ExceptionInfo.class)) + public ResponseEntity getDistributionSetType(@PathVariable final Long distributionSetTypeId) { + final DistributionSetType foundType = findDistributionSetTypeWithExceptionIfNotFound(distributionSetTypeId); + + return new ResponseEntity<>(DistributionSetTypeMapper.toResponse(foundType), HttpStatus.OK); + } + + /** + * Handles the DELETE request for a single Distribution Set Type within SP. + * + * @param distributionSetTypeId + * the ID of the module to retrieve + * @return status OK if delete as sucessfull. + * + */ + @RequestMapping(method = RequestMethod.DELETE, value = "/{distributionSetTypeId}") + @ApiOperation(value = "Delete distribution set type", notes = "Handles the DELETE request for a single distribution set type within SP. Required Permission: " + + SpPermission.DELETE_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not found distribution set type", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity deleteDistributionSetType(@PathVariable final Long distributionSetTypeId) { + final DistributionSetType module = findDistributionSetTypeWithExceptionIfNotFound(distributionSetTypeId); + + distributionSetManagement.deleteDistributionSetType(module); + + return new ResponseEntity<>(HttpStatus.OK); + } + + /** + * Handles the PUT request of updating a Distribution Set Type within SP. + * + * @param distributionSetTypeId + * the ID of the software module in the URL + * @param restDistributionSetType + * the module type to be updated. + * @return status OK if update is successful + */ + @RequestMapping(method = RequestMethod.PUT, value = "/{distributionSetTypeId}", consumes = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }, produces = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(value = "Update distribution set type", notes = "Handles the PUT request for a single distribution set type within SP. Required Permission: " + + SpPermission.UPDATE_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found distribution set type", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity updateDistributionSetType( + @PathVariable final Long distributionSetTypeId, + @RequestBody final DistributionSetTypeRequestBodyUpdate restDistributionSetType) { + final DistributionSetType type = findDistributionSetTypeWithExceptionIfNotFound(distributionSetTypeId); + + // only description can be modified + if (restDistributionSetType.getDescription() != null) { + type.setDescription(restDistributionSetType.getDescription()); + } + + final DistributionSetType updatedDistributionSetType = distributionSetManagement + .updateDistributionSetType(type); + + // we flush to ensure that entity is generated and we can return ID etc. + entityManager.flush(); + + return new ResponseEntity<>(DistributionSetTypeMapper.toResponse(updatedDistributionSetType), HttpStatus.OK); + } + + /** + * Handles the POST request of creating new {@link DistributionSetType}s + * within SP. The request body must always be a list of types. + * + * @param distributionSetTypes + * the modules to be created. + * @return In case all modules could successful created the ResponseEntity + * with status code 201 - Created but without ResponseBody. In any + * failure the JsonResponseExceptionHandler is handling the + * response. + */ + @RequestMapping(method = RequestMethod.POST, consumes = { "application/hal+json", MediaType.APPLICATION_JSON_VALUE }, produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = DistributionSetTypesRest.class, value = "Create distribution set types", notes = "Handles the POST request for creating new distribution set types within SP. The request body must always be a list of types. Required Permission: " + + SpPermission.CREATE_REPOSITORY) + @ApiResponses({ + @ApiResponse(code = 404, message = "Not Found Distribution Set Type", response = ExceptionInfo.class), + @ApiResponse(code = 409, message = "Conflict Distribution Set Type already exists", response = ExceptionInfo.class) }) + @Transactional + public ResponseEntity createDistributionSetTypes( + @RequestBody final List distributionSetTypes) { + + final List createdSoftwareModules = distributionSetManagement + .createDistributionSetTypes(DistributionSetTypeMapper.smFromRequest(softwareManagement, + distributionSetTypes)); + + // we flush to ensure that entity is generated and we can return ID etc. + entityManager.flush(); + + return new ResponseEntity<>(DistributionSetTypeMapper.toTypesResponse(createdSoftwareModules), + HttpStatus.CREATED); + } + + private DistributionSetType findDistributionSetTypeWithExceptionIfNotFound(final Long distributionSetTypeId) { + final DistributionSetType module = distributionSetManagement.findDistributionSetTypeById(distributionSetTypeId); + if (module == null) { + throw new EntityNotFoundException("DistributionSetType with Id {" + distributionSetTypeId + + "} does not exist"); + } + return module; + } + + /** + * Handles the GET request of retrieving the list of mandatory software + * module types in that distribution set type. + * + * @param distributionSetTypeId + * of the {@link DistributionSetType}. + * @return Unpaged list of module types and OK in case of success. + */ + @RequestMapping(method = RequestMethod.GET, value = "/{distributionSetTypeId}/" + + RestConstants.DISTRIBUTIONSETTYPE_V1_MANDATORY_MODULE_TYPES, produces = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = ActionPagedList.class, value = "Lists all mandatory software module types", notes = "Handles the GET request of retrieving the list of mandatory software module types in that distribution set type. Required Permission: " + + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found distribution set type", response = ExceptionInfo.class)) + public ResponseEntity getMandatoryModules(@PathVariable final Long distributionSetTypeId) { + + final DistributionSetType foundType = findDistributionSetTypeWithExceptionIfNotFound(distributionSetTypeId); + + final SoftwareModuleTypesRest rest = new SoftwareModuleTypesRest(); + + rest.addAll(SoftwareModuleTypeMapper.toListResponse(foundType.getMandatoryModuleTypes())); + return new ResponseEntity<>(rest, HttpStatus.OK); + } + + /** + * Handles the GET request of retrieving the single mandatory software + * module type in that distribution set type. + * + * @param distributionSetTypeId + * of the {@link DistributionSetType}. + * @param softwareModuleTypeId + * of {@link SoftwareModuleType}. + * @return Unpaged list of module types and OK in case of success. + */ + @RequestMapping(method = RequestMethod.GET, value = "/{distributionSetTypeId}/" + + RestConstants.DISTRIBUTIONSETTYPE_V1_MANDATORY_MODULE_TYPES + "/{softwareModuleTypeId}", produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = ActionPagedList.class, value = "Retrieve mandatory software module type", notes = " Handles the GET request of retrieving the single mandatory software module type in that distribution set type. Required Permission: " + + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found distribution set type", response = ExceptionInfo.class)) + public ResponseEntity getMandatoryModule(@PathVariable final Long distributionSetTypeId, + @PathVariable final Long softwareModuleTypeId) { + + final DistributionSetType foundType = findDistributionSetTypeWithExceptionIfNotFound(distributionSetTypeId); + + final SoftwareModuleType foundSmType = findSoftwareModuleTypeWithExceptionIfNotFound(softwareModuleTypeId); + + if (!foundType.containsMandatoryModuleType(foundSmType)) { + throw new EntityNotFoundException( + "Software module with given ID is not part of this distribution set type!"); + } + + return new ResponseEntity(SoftwareModuleTypeMapper.toResponse(foundSmType), + HttpStatus.OK); + } + + /** + * Handles the GET request of retrieving the single optional software module + * type in that distribution set type. + * + * @param distributionSetTypeId + * of the {@link DistributionSetType}. + * @param softwareModuleTypeId + * of {@link SoftwareModuleType}. + * @return Unpaged list of module types and OK in case of success. + */ + @RequestMapping(method = RequestMethod.GET, value = "/{distributionSetTypeId}/" + + RestConstants.DISTRIBUTIONSETTYPE_V1_OPTIONAL_MODULE_TYPES + "/{softwareModuleTypeId}", produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = ActionPagedList.class, value = "Retrieve optional software module type", notes = " Handles the GET request of retrieving the single optional software module type in that distribution set type. Required Permission: " + + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found distribution set type", response = ExceptionInfo.class)) + public ResponseEntity getOptionalModule(@PathVariable final Long distributionSetTypeId, + @PathVariable final Long softwareModuleTypeId) { + + final DistributionSetType foundType = findDistributionSetTypeWithExceptionIfNotFound(distributionSetTypeId); + + final SoftwareModuleType foundSmType = findSoftwareModuleTypeWithExceptionIfNotFound(softwareModuleTypeId); + + if (!foundType.containsOptionalModuleType(foundSmType)) { + throw new EntityNotFoundException( + "Software module with given ID is not part of this distribution set type!"); + } + + return new ResponseEntity(SoftwareModuleTypeMapper.toResponse(foundSmType), + HttpStatus.OK); + } + + /** + * Handles the GET request of retrieving the list of optional software + * module types in that distribution set type. + * + * @param distributionSetTypeId + * of the {@link DistributionSetType}. + * @return Unpaged list of module types and OK in case of success. + */ + @RequestMapping(method = RequestMethod.GET, value = "/{distributionSetTypeId}/" + + RestConstants.DISTRIBUTIONSETTYPE_V1_OPTIONAL_MODULE_TYPES, produces = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = ActionPagedList.class, value = "Lists all optional software module types", notes = "Handles the GET request of retrieving the list of optional software module types in that distribution set type. Required Permission: " + + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found distribution set type", response = ExceptionInfo.class)) + public ResponseEntity getOptionalModules(@PathVariable final Long distributionSetTypeId) { + + final DistributionSetType foundType = findDistributionSetTypeWithExceptionIfNotFound(distributionSetTypeId); + + final SoftwareModuleTypesRest rest = new SoftwareModuleTypesRest(); + + rest.addAll(SoftwareModuleTypeMapper.toListResponse(foundType.getOptionalModuleTypes())); + return new ResponseEntity<>(rest, HttpStatus.OK); + } + + /** + * Handles DELETE request for removing a mandatory module from the + * {@link DistributionSetType}. + * + * @param distributionSetTypeId + * of the {@link DistributionSetType}. + * @param softwareModuleTypeId + * of the {@link SoftwareModuleType} to remove + * + * @return OK if the request was successful + */ + @RequestMapping(method = RequestMethod.DELETE, value = "/{distributionSetTypeId}/" + + RestConstants.DISTRIBUTIONSETTYPE_V1_MANDATORY_MODULE_TYPES + "/{softwareModuleTypeId}", produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = ActionPagedList.class, value = "Remove mandatory module from distribution set type", notes = "Handles the GET request of retrieving the list of software module types in that distribution set. " + + "Note that a DS type cannot be changed after it has been used by a DS. Required Permission: " + + SpPermission.UPDATE_REPOSITORY + " and " + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found distribution set type", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity removeMandatoryModule(@PathVariable final Long distributionSetTypeId, + @PathVariable final Long softwareModuleTypeId) { + + final DistributionSetType foundType = findDistributionSetTypeWithExceptionIfNotFound(distributionSetTypeId); + + foundType.removeModuleType(softwareModuleTypeId); + + distributionSetManagement.updateDistributionSetType(foundType); + + return new ResponseEntity<>(HttpStatus.OK); + } + + /** + * Handles DELETE request for removing an optional module from the + * {@link DistributionSetType}. + * + * @param distributionSetTypeId + * of the {@link DistributionSetType}. + * @param softwareModuleTypeId + * of the {@link SoftwareModuleType} to remove + * + * @return OK if the request was successful + */ + @RequestMapping(method = RequestMethod.DELETE, value = "/{distributionSetTypeId}/" + + RestConstants.DISTRIBUTIONSETTYPE_V1_OPTIONAL_MODULE_TYPES + "/{softwareModuleTypeId}", produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = ActionPagedList.class, value = "Remove optional module from distribution set type", notes = "Handles DELETE request for removing an optional module from the distribution set type." + + "Note that a DS type cannot be changed after it has been used by a DS. Required Permission: " + + SpPermission.UPDATE_REPOSITORY + " and " + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found distribution set type", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity removeOptionalModule(@PathVariable final Long distributionSetTypeId, + @PathVariable final Long softwareModuleTypeId) { + return removeMandatoryModule(distributionSetTypeId, softwareModuleTypeId); + } + + /** + * Handles the POST request for adding a mandatory software module type to a + * distribution set type. + * + * @param distributionSetTypeId + * of the {@link DistributionSetType}. + * @param smtId + * of the {@link SoftwareModuleType} to add + * + * @return OK if the request was successful + */ + @RequestMapping(method = RequestMethod.POST, value = "/{distributionSetTypeId}/" + + RestConstants.DISTRIBUTIONSETTYPE_V1_MANDATORY_MODULE_TYPES, consumes = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }, produces = { "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = Void.class, value = "Add mandatory software module type", notes = "Handles the POST request for adding a mandatory software module type to a distribution set type." + + "Note that a DS type cannot be changed after it has been used by a DS. Required Permission: " + + SpPermission.UPDATE_REPOSITORY + " and " + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found DS type or SP type", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity addMandatoryModule(@PathVariable final Long distributionSetTypeId, + @RequestBody @ApiParam(value = "Software module type ID", required = true) final IdRest smtId) { + + final DistributionSetType foundType = findDistributionSetTypeWithExceptionIfNotFound(distributionSetTypeId); + + final SoftwareModuleType smType = findSoftwareModuleTypeWithExceptionIfNotFound(smtId.getId()); + + foundType.addMandatoryModuleType(smType); + + return new ResponseEntity<>(HttpStatus.OK); + } + + /** + * Handles the POST request for adding an optional software module type to a + * distribution set type. + * + * @param distributionSetTypeId + * of the {@link DistributionSetType}. + * @param smtId + * of the {@link SoftwareModuleType} to add + * + * @return OK if the request was successful + */ + @RequestMapping(method = RequestMethod.POST, value = "/{distributionSetTypeId}/" + + RestConstants.DISTRIBUTIONSETTYPE_V1_OPTIONAL_MODULE_TYPES, consumes = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }, produces = { "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = Void.class, value = "Add optional software module type", notes = "Handles the POST request for adding an optional software module type to a distribution set type." + + "Note that a DS type cannot be changed after it has been used by a DS. Required Permission: " + + SpPermission.UPDATE_REPOSITORY + " and " + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found DS type or SP type", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity addOptionalModule(@PathVariable final Long distributionSetTypeId, + @RequestBody @ApiParam(value = "Software module type ID", required = true) final IdRest smtId) { + + final DistributionSetType foundType = findDistributionSetTypeWithExceptionIfNotFound(distributionSetTypeId); + + final SoftwareModuleType smType = findSoftwareModuleTypeWithExceptionIfNotFound(smtId.getId()); + + foundType.addOptionalModuleType(smType); + + return new ResponseEntity<>(HttpStatus.OK); + + } + + private SoftwareModuleType findSoftwareModuleTypeWithExceptionIfNotFound(final Long softwareModuleTypeId) { + final SoftwareModuleType module = softwareManagement.findSoftwareModuleTypeById(softwareModuleTypeId); + if (module == null) { + throw new EntityNotFoundException("SoftwareModuleType with Id {" + softwareModuleTypeId + + "} does not exist"); + } + return module; + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/DownloadResource.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/DownloadResource.java new file mode 100755 index 000000000..14b1dcbc6 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/DownloadResource.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import java.io.IOException; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.IOUtils; +import org.eclipse.hawkbit.artifact.repository.ArtifactRepository; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; +import org.eclipse.hawkbit.cache.CacheConstants; +import org.eclipse.hawkbit.cache.DownloadArtifactCache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.cache.Cache; +import org.springframework.cache.Cache.ValueWrapper; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * A resource for download artifacts. + * + * + * + */ +@RequestMapping(RestConstants.DOWNLOAD_ID_V1_REQUEST_MAPPING_BASE) +@RestController +public class DownloadResource { + + private static final Logger LOGGER = LoggerFactory.getLogger(DownloadResource.class); + + @Autowired + private ArtifactRepository artifactRepository; + + @Autowired + @Qualifier(CacheConstants.DOWNLOAD_ID_CACHE) + private Cache cache; + + /** + * Handles the GET request for downloading an artifact. + * + * @param downloadId + * the generated download id + * @param response + * of the servlet + * @return {@link ResponseEntity} with status {@link HttpStatus#OK} if + * successful + */ + @RequestMapping(method = RequestMethod.GET, value = RestConstants.DOWNLOAD_ID_V1_REQUEST_MAPPING) + @ResponseBody + public ResponseEntity downloadArtifactByDownloadId(@PathVariable final String downloadId, + final HttpServletResponse response) { + try { + final ValueWrapper cacheWrapper = cache.get(downloadId); + if (cacheWrapper == null) { + LOGGER.warn("Download Id {} could not be found", downloadId); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + final DownloadArtifactCache artifactCache = (DownloadArtifactCache) cacheWrapper.get(); + DbArtifact artifact = null; + switch (artifactCache.getDownloadType()) { + case BY_SHA1: + artifact = artifactRepository.getArtifactBySha1(artifactCache.getId()); + break; + + default: + LOGGER.warn("Download Type {} not supported", artifactCache.getDownloadType()); + break; + } + + if (artifact == null) { + LOGGER.warn("Artifact with cached id {} and download type {} could not be found.", + artifactCache.getId(), artifactCache.getDownloadType()); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + try { + IOUtils.copy(artifact.getFileInputStream(), response.getOutputStream()); + } catch (final IOException e) { + LOGGER.error("Cannot copy streams", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } finally { + cache.evict(downloadId); + } + + return ResponseEntity.ok().build(); + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/EnableRestResources.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/EnableRestResources.java new file mode 100755 index 000000000..8f9952054 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/EnableRestResources.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Controller; + +/** + * Annotation to enable {@link ComponentScan} in the resource package to setup + * all {@link Controller} annotated classes and setup the REST-Resources for the + * Management API. + * + * + * + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Configuration +@ComponentScan +public @interface EnableRestResources { + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/MessageNotReadableException.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/MessageNotReadableException.java new file mode 100755 index 000000000..023b67352 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/MessageNotReadableException.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * Exception which is thrown in case an request body is not well formaned and + * cannot be parsed. + * + * + * + * + */ +public class MessageNotReadableException extends SpServerRtException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new MessageNotReadableException with + * {@link SpServerError#SP_REST_BODY_NOT_READABLE} error. + */ + public MessageNotReadableException() { + super(SpServerError.SP_REST_BODY_NOT_READABLE); + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/OffsetBasedPageRequest.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/OffsetBasedPageRequest.java new file mode 100755 index 000000000..5ab6b33c6 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/OffsetBasedPageRequest.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; + +/** + * An implementation of the {@link PageRequest} which is offset based by means + * the offset is given and not the page number as in the original + * {@link PageRequest} implemntation where the offset is generated. Due that the + * REST-API is working with {@code offset} and {@code limit} parameter we need + * an offset based page request for JPA. + * + * + * + * + */ +public final class OffsetBasedPageRequest extends PageRequest { + + private static final long serialVersionUID = 1L; + private final int offset; + + /** + * Creates a new {@link OffsetBasedPageRequest}. Offsets are zero indexed, + * thus providing 0 for {@code offset} will return the first entry. + * + * @param offset + * zero-based offset index. + * @param limit + * the limit of the page to be returned. + */ + public OffsetBasedPageRequest(final int offset, final int limit) { + this(offset, limit, null); + } + + /** + * Creates a new {@link OffsetBasedPageRequest}. Offsets are zero indexed, + * thus providing 0 for {@code offset} will return the first entry. + * + * @param offset + * zero-based offset index. + * @param limit + * the limit of the page to be returned. + * @param sort + * sort can be {@literal null}. + */ + public OffsetBasedPageRequest(final int offset, final int limit, final Sort sort) { + super(0, limit, sort); + this.offset = offset; + } + + @Override + public int getOffset() { + return offset; + } + + @Override + public String toString() { + return "OffsetBasedPageRequest [offset=" + offset + ", getPageSize()=" + getPageSize() + ", getPageNumber()=" + + getPageNumber() + "]"; + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/PagingUtility.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/PagingUtility.java new file mode 100755 index 000000000..afefedb5c --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/PagingUtility.java @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import org.eclipse.hawkbit.repository.ActionFields; +import org.eclipse.hawkbit.repository.ActionStatusFields; +import org.eclipse.hawkbit.repository.DistributionSetFields; +import org.eclipse.hawkbit.repository.DistributionSetMetadataFields; +import org.eclipse.hawkbit.repository.SoftwareModuleFields; +import org.eclipse.hawkbit.repository.SoftwareModuleMetadataFields; +import org.eclipse.hawkbit.repository.TargetFields; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; + +/** + * Utility class for for paged body generation. + * + * + * + * + * + * + */ +public final class PagingUtility { + /* + * utility constructor private. + */ + private PagingUtility() { + } + + static int sanitizeOffsetParam(final int offset) { + if (offset < 0) { + return Integer.parseInt(RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET); + } + return offset; + } + + static int sanitizePageLimitParam(final int pageLimit) { + if (pageLimit < 1) { + return Integer.parseInt(RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT); + } else if (pageLimit > RestConstants.REQUEST_PARAMETER_PAGING_MAX_LIMIT) { + return RestConstants.REQUEST_PARAMETER_PAGING_MAX_LIMIT; + } + return pageLimit; + } + + static Sort sanitizeTargetSortParam(final String sortParam) { + final Sort sorting; + if (sortParam != null) { + sorting = new Sort(SortUtility.parse(TargetFields.class, sortParam)); + } else { + // default sort + sorting = new Sort(Direction.ASC, TargetFields.NAME.getFieldName()); + } + return sorting; + } + + static Sort sanitizeSoftwareModuleSortParam(final String sortParam) { + final Sort sorting; + if (sortParam != null) { + sorting = new Sort(SortUtility.parse(SoftwareModuleFields.class, sortParam)); + } else { + // default sort + sorting = new Sort(Direction.ASC, SoftwareModuleFields.NAME.getFieldName()); + } + return sorting; + } + + static Sort sanitizeDistributionSetSortParam(final String sortParam) { + final Sort sorting; + if (sortParam != null) { + sorting = new Sort(SortUtility.parse(DistributionSetFields.class, sortParam)); + } else { + // default sort + sorting = new Sort(Direction.ASC, DistributionSetFields.NAME.getFieldName()); + } + return sorting; + } + + static Sort sanitizeActionSortParam(final String sortParam) { + final Sort sorting; + if (sortParam != null) { + sorting = new Sort(SortUtility.parse(ActionFields.class, sortParam)); + } else { + // default sort + sorting = new Sort(Direction.ASC, ActionFields.ID.getFieldName()); + } + return sorting; + } + + static Sort sanitizeActionStatusSortParam(final String sortParam) { + final Sort sorting; + if (sortParam != null) { + sorting = new Sort(SortUtility.parse(ActionStatusFields.class, sortParam)); + } else { + // default sort + sorting = new Sort(Direction.ASC, ActionStatusFields.ID.getFieldName()); + } + return sorting; + } + + static Sort sanitizeDistributionSetMetadataSortParam(final String sortParam) { + final Sort sorting; + if (sortParam != null) { + sorting = new Sort(SortUtility.parse(DistributionSetMetadataFields.class, sortParam)); + } else { + // default sort + sorting = new Sort(Direction.ASC, DistributionSetMetadataFields.KEY.getFieldName()); + } + return sorting; + } + + static Sort sanitizeSoftwareModuleMetadataSortParam(final String sortParam) { + final Sort sorting; + if (sortParam != null) { + sorting = new Sort(SortUtility.parse(SoftwareModuleMetadataFields.class, sortParam)); + } else { + // default sort + sorting = new Sort(Direction.ASC, SoftwareModuleMetadataFields.KEY.getFieldName()); + } + return sorting; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/ResponseExceptionHandler.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/ResponseExceptionHandler.java new file mode 100755 index 000000000..a6a9fd9d1 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/ResponseExceptionHandler.java @@ -0,0 +1,169 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.tomcat.util.http.fileupload.FileUploadBase.FileSizeLimitExceededException; +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; +import org.eclipse.hawkbit.rest.resource.model.ExceptionInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.multipart.MultipartException; + +/** + * General controller advice for exception handling. + * + * + * + * + */ +@ControllerAdvice +public class ResponseExceptionHandler { + + private static final Logger LOG = LoggerFactory.getLogger(ResponseExceptionHandler.class); + private static final Map ERROR_TO_HTTP_STATUS = new HashMap<>(); + private static final HttpStatus DEFAULT_RESPONSE_STATUS = HttpStatus.INTERNAL_SERVER_ERROR; + + static { + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REPO_ENTITY_NOT_EXISTS, HttpStatus.NOT_FOUND); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REPO_ENTITY_ALRREADY_EXISTS, HttpStatus.CONFLICT); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REPO_ENTITY_READ_ONLY, HttpStatus.FORBIDDEN); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REST_SORT_PARAM_INVALID_DIRECTION, HttpStatus.BAD_REQUEST); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REST_SORT_PARAM_INVALID_FIELD, HttpStatus.BAD_REQUEST); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REST_SORT_PARAM_SYNTAX, HttpStatus.BAD_REQUEST); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REST_RSQL_PARAM_INVALID_FIELD, HttpStatus.BAD_REQUEST); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REST_RSQL_SEARCH_PARAM_SYNTAX, HttpStatus.BAD_REQUEST); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_INSUFFICIENT_PERMISSION, HttpStatus.FORBIDDEN); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_UPLOAD_FAILED, HttpStatus.INTERNAL_SERVER_ERROR); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_UPLOAD_FAILED_SHA1_MATCH, HttpStatus.BAD_REQUEST); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_UPLOAD_FAILED_MD5_MATCH, HttpStatus.BAD_REQUEST); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_DELETE_FAILED, HttpStatus.INTERNAL_SERVER_ERROR); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_LOAD_FAILED, HttpStatus.INTERNAL_SERVER_ERROR); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ACTION_STATUS_TO_MANY_ENTRIES, HttpStatus.FORBIDDEN); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ATTRIBUTES_TO_MANY_ENTRIES, HttpStatus.FORBIDDEN); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ACTION_NOT_CANCELABLE, HttpStatus.METHOD_NOT_ALLOWED); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_DS_CREATION_FAILED_MISSING_MODULE, HttpStatus.BAD_REQUEST); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_DS_MODULE_UNSUPPORTED, HttpStatus.BAD_REQUEST); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_DS_TYPE_UNDEFINED, HttpStatus.BAD_REQUEST); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REPO_TENANT_NOT_EXISTS, HttpStatus.BAD_REQUEST); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ENTITY_LOCKED, HttpStatus.LOCKED); + + } + + private static HttpStatus getStatusOrDefault(final SpServerError error) { + return ERROR_TO_HTTP_STATUS.getOrDefault(error, DEFAULT_RESPONSE_STATUS); + } + + /** + * method for handling exception of type SpServerRtException. Called by the + * Spring-Framework for exception handling. + * + * @param request + * the Http request + * @param ex + * the exception which occurred + * + * @return the entity to be responded containing the exception information + * as entity. + */ + @ExceptionHandler(SpServerRtException.class) + public ResponseEntity handleSpServerRtExceptions(final HttpServletRequest request, + final Exception ex) { + LOG.debug("Handling exception of request {}", request.getRequestURL()); + final ExceptionInfo response = new ExceptionInfo(); + final HttpStatus responseStatus; + response.setMessage(ex.getMessage()); + response.setExceptionClass(ex.getClass().getName()); + if (ex instanceof SpServerRtException) { + responseStatus = getStatusOrDefault(((SpServerRtException) ex).getError()); + response.setErrorCode(((SpServerRtException) ex).getError().getKey()); + } else { + responseStatus = DEFAULT_RESPONSE_STATUS; + } + return new ResponseEntity(response, responseStatus); + } + + /** + * Method for handling exception of type HttpMessageNotReadableException + * which is thrown in case the request body is not well formed and cannot be + * deserialized. Called by the Spring-Framework for exception handling. + * + * @param request + * the Http request + * @param ex + * the exception which occurred + * @return the entity to be responded containing the exception information + * as entity. + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleHttpMessageNotReadableException(final HttpServletRequest request, + final Exception ex) { + LOG.debug("Handling exception {} of request {}", ex.getClass().getName(), request.getRequestURL()); + final ExceptionInfo response = new ExceptionInfo(); + response.setErrorCode(SpServerError.SP_REST_BODY_NOT_READABLE.getKey()); + response.setMessage(SpServerError.SP_REST_BODY_NOT_READABLE.getMessage()); + response.setExceptionClass(MessageNotReadableException.class.getName()); + return new ResponseEntity(response, HttpStatus.BAD_REQUEST); + } + + /** + * Method for handling exception of type {@link MultipartException} which is + * thrown in case the request body is not well formed and cannot be + * deserialized. Called by the Spring-Framework for exception handling. + * + * @param request + * the Http request + * @param ex + * the exception which occurred + * @return the entity to be responded containing the exception information + * as entity. + */ + @ExceptionHandler(MultipartException.class) + public ResponseEntity handleFileLimitExceededException(final HttpServletRequest request, + final Exception ex) { + LOG.debug("Handling exception {} of request {}", ex.getClass().getName(), request.getRequestURL()); + + final ExceptionInfo response = new ExceptionInfo(); + + if (searchForCause(ex, FileSizeLimitExceededException.class)) { + response.setErrorCode(SpServerError.SP_ARTIFACT_UPLOAD_FILE_LIMIT_EXCEEDED.getKey()); + response.setMessage(SpServerError.SP_ARTIFACT_UPLOAD_FILE_LIMIT_EXCEEDED.getMessage()); + response.setExceptionClass(FileSizeLimitExceededException.class.getName()); + } else { + response.setErrorCode(SpServerError.SP_ARTIFACT_UPLOAD_FAILED.getKey()); + response.setMessage(SpServerError.SP_ARTIFACT_UPLOAD_FAILED.getMessage()); + response.setExceptionClass(MultipartException.class.getName()); + } + + return new ResponseEntity(response, HttpStatus.BAD_REQUEST); + } + + private static boolean searchForCause(final Throwable t, final Class lookFor) { + if (t != null && t.getCause() != null) { + if (t.getCause().getClass().isAssignableFrom(lookFor)) { + return true; + } else { + return searchForCause(t.getCause(), lookFor); + } + } + + return false; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/RestConstants.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/RestConstants.java new file mode 100755 index 000000000..d026a0883 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/RestConstants.java @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +/** + * Constants for RESTful API. + * + * + * + */ +public final class RestConstants { + + /** + * API version definition. We are using only major versions. + */ + public static final String API_VERSION = "v1"; + + /** + * The base URL mapping for the spring acuator management context path. + */ + public static final String BASE_SYSTEM_MAPPING = "/system"; + + /** + * URL mapping for system admin operations. + */ + public static final String SYSTEM_ADMIN_MAPPING = BASE_SYSTEM_MAPPING + "/admin"; + + /** + * The base URL mapping of the SP rest resources. + */ + public static final String BASE_V1_REQUEST_MAPPING = "/rest/v1"; + + /** + * The default limit parameter in case the limit parameter is not present in + * the request. + * + * @see #REQUEST_PARAMETER_PAGING_LIMIT + */ + public static final int REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT_VALUE = 50; + + /** + * String representation of + * {@link #REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT_VALUE}. + */ + public static final String REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT = "50"; + + /** + * The default offset parameter in case the offset parameter is not present + * in the request. + * + * @see #REQUEST_PARAMETER_PAGING_OFFSET + */ + public static final String REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET = "0"; + + /** + * Limit http parameter for the limitation of returned values for a paged + * request. + */ + public static final String REQUEST_PARAMETER_PAGING_LIMIT = "limit"; + /** + * The maximum limit of entities returned by rest resources. + */ + public static final int REQUEST_PARAMETER_PAGING_MAX_LIMIT = 500; + + /** + * Paging http parameter for the offset for a paged request. + */ + public static final String REQUEST_PARAMETER_PAGING_OFFSET = "offset"; + + /** + * The request parameter for sorting. The value of the sort parameter must + * be in the following pattern. Example: + * http://www.bosch.com/iap/sp/rest/targets?sort=field_1:ASC,field_2:DESC, + * field_3:ASC + */ + public static final String REQUEST_PARAMETER_SORTING = "sort"; + + /** + * The request parameter for searching. The value of the search parameter + * must be in the FIQL syntax. + */ + public static final String REQUEST_PARAMETER_SEARCH = "q"; + + /** + * The target URL mapping, href link for assigned distribution set. + */ + public static final String TARGET_V1_ASSIGNED_DISTRIBUTION_SET = "assignedDS"; + /** + * The target URL mapping, href link for installed distribution set. + */ + public static final String TARGET_V1_INSTALLED_DISTRIBUTION_SET = "installedDS"; + /** + * The target URL mapping, href link for target attributes. + */ + public static final String TARGET_V1_ATTRIBUTES = "attributes"; + /** + * The target URL mapping, href link for target actions. + */ + public static final String TARGET_V1_ACTIONS = "actions"; + /** + * The target URL mapping, href link for canceled actions. + */ + public static final String TARGET_V1_CANCELED_ACTION = "canceledaction"; + /** + * The target URL mapping, href link for canceled actions. + */ + public static final String TARGET_V1_ACTION_STATUS = "status"; + + /** + * The target URL mapping rest resource. + */ + public static final String TARGET_V1_REQUEST_MAPPING = BASE_V1_REQUEST_MAPPING + "/targets"; + /** + * The software module URL mapping rest resource. + */ + public static final String SOFTWAREMODULE_V1_REQUEST_MAPPING = BASE_V1_REQUEST_MAPPING + "/softwaremodules"; + + /** + * The software module type URL mapping rest resource. + */ + public static final String SOFTWAREMODULETYPE_V1_REQUEST_MAPPING = BASE_V1_REQUEST_MAPPING + "/softwaremoduletypes"; + + public static final String DISTRIBUTIONSETTYPE_V1_REQUEST_MAPPING = BASE_V1_REQUEST_MAPPING + + "/distributionsettypes"; + + /** + * The software module URL mapping rest resource. + */ + public static final String DISTRIBUTIONSET_V1_REQUEST_MAPPING = BASE_V1_REQUEST_MAPPING + "/distributionsets"; + + /** + * The target URL mapping, href link for artifact download. + */ + public static final String SOFTWAREMODULE_V1_ARTIFACT = "artifacts"; + + /** + * The target URL mapping, href link for type information. + */ + public static final String SOFTWAREMODULE_V1_TYPE = "type"; + + public static final String DISTRIBUTIONSETTYPE_V1_OPTIONAL_MODULES = "optionalmodules"; + + public static final String DISTRIBUTIONSETTYPE_V1_MANDATORY_MODULES = "mandatorymodules"; + + public static final String DISTRIBUTIONSETTYPE_V1_OPTIONAL_MODULE_TYPES = "optionalmoduletypes"; + + public static final String DISTRIBUTIONSETTYPE_V1_MANDATORY_MODULE_TYPES = "mandatorymoduletypes"; + + public static final String DOWNLOAD_ID_V1_REQUEST_MAPPING_BASE = "/api/" + API_VERSION + "/downloadserver/"; + public static final String DOWNLOAD_ID_V1_REQUEST_MAPPING = "downloadId/{downloadId}"; + + // constant class, private constructor. + private RestConstants() { + + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/RestModelMapper.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/RestModelMapper.java new file mode 100755 index 000000000..52668f1f1 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/RestModelMapper.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import org.eclipse.hawkbit.repository.model.BaseEntity; +import org.eclipse.hawkbit.repository.model.NamedEntity; +import org.eclipse.hawkbit.rest.resource.model.BaseEntityRest; +import org.eclipse.hawkbit.rest.resource.model.NamedEntityRest; + +/** + * A mapper which maps repository model to RESTful model representation and + * back. + * + * + * + * + */ +final class RestModelMapper { + + // private constructor, utility class + private RestModelMapper() { + + } + + static void mapBaseToBase(final BaseEntityRest response, final BaseEntity base) { + response.setCreatedBy(base.getCreatedBy()); + response.setLastModifiedBy(base.getLastModifiedBy()); + if (base.getCreatedAt() != null) { + response.setCreatedAt(base.getCreatedAt()); + } + if (base.getLastModifiedAt() != null) { + response.setLastModifiedAt(base.getLastModifiedAt()); + } + } + + static void mapNamedToNamed(final NamedEntityRest response, final NamedEntity base) { + mapBaseToBase(response, base); + + response.setName(base.getName()); + response.setDescription(base.getDescription()); + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleMapper.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleMapper.java new file mode 100755 index 000000000..9bdf7c025 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleMapper.java @@ -0,0 +1,186 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.Artifact; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.rest.resource.model.MetadataRest; +import org.eclipse.hawkbit.rest.resource.model.artifact.ArtifactHash; +import org.eclipse.hawkbit.rest.resource.model.artifact.ArtifactRest; +import org.eclipse.hawkbit.rest.resource.model.artifact.ArtifactsRest; +import org.eclipse.hawkbit.rest.resource.model.softwaremodule.SoftwareModuleRequestBodyPost; +import org.eclipse.hawkbit.rest.resource.model.softwaremodule.SoftwareModuleRest; +import org.eclipse.hawkbit.rest.resource.model.softwaremodule.SoftwareModulesRest; + +/** + * A mapper which maps repository model to RESTful model representation and + * back. + * + * + * + * + */ +final class SoftwareModuleMapper { + private SoftwareModuleMapper() { + // Utility class + } + + private static SoftwareModuleType getSoftwareModuleTypeFromKeyString(final String type, + final SoftwareManagement softwareManagement) { + + final SoftwareModuleType smType = softwareManagement.findSoftwareModuleTypeByKey(type.trim()); + + if (smType == null) { + throw new EntityNotFoundException(type.trim()); + } + + return smType; + } + + static SoftwareModule fromRequest(final SoftwareModuleRequestBodyPost smsRest, + final SoftwareManagement softwareManagement) { + return new SoftwareModule(getSoftwareModuleTypeFromKeyString(smsRest.getType(), softwareManagement), + smsRest.getName(), smsRest.getVersion(), smsRest.getDescription(), smsRest.getVendor()); + } + + static List fromRequestSwMetadata(final SoftwareModule sw, + final List metadata) { + final List mappedList = new ArrayList<>(metadata.size()); + for (final MetadataRest metadataRest : metadata) { + if (metadataRest.getKey() == null) { + throw new IllegalArgumentException("the key of the metadata must be present"); + } + mappedList.add(new SoftwareModuleMetadata(metadataRest.getKey(), sw, metadataRest.getValue())); + } + return mappedList; + } + + static List smFromRequest(final Iterable smsRest, + final SoftwareManagement softwareManagement) { + final List mappedList = new ArrayList<>(); + for (final SoftwareModuleRequestBodyPost smRest : smsRest) { + mappedList.add(fromRequest(smRest, softwareManagement)); + } + return mappedList; + } + + static List toResponse(final List baseSoftareModules) { + final List mappedList = new ArrayList<>(); + if (baseSoftareModules != null) { + for (final SoftwareModule target : baseSoftareModules) { + final SoftwareModuleRest response = toResponse(target); + + mappedList.add(response); + } + } + return mappedList; + } + + static SoftwareModulesRest toResponseSoftwareModules(final Iterable softwareModules) { + final SoftwareModulesRest response = new SoftwareModulesRest(); + for (final SoftwareModule softwareModule : softwareModules) { + response.add(toResponse(softwareModule)); + } + return response; + } + + static List toResponseSwMetadata(final List metadata) { + final List mappedList = new ArrayList<>(metadata.size()); + for (final SoftwareModuleMetadata distributionSetMetadata : metadata) { + mappedList.add(toResponseSwMetadata(distributionSetMetadata)); + } + return mappedList; + } + + static MetadataRest toResponseSwMetadata(final SoftwareModuleMetadata metadata) { + final MetadataRest metadataRest = new MetadataRest(); + metadataRest.setKey(metadata.getId().getKey()); + metadataRest.setValue(metadata.getValue()); + return metadataRest; + } + + static SoftwareModuleRest toResponse(final SoftwareModule baseSofwareModule) { + if (baseSofwareModule == null) { + return null; + } + + final SoftwareModuleRest response = new SoftwareModuleRest(); + RestModelMapper.mapNamedToNamed(response, baseSofwareModule); + response.setModuleId(baseSofwareModule.getId()); + response.setVersion(baseSofwareModule.getVersion()); + response.setType(baseSofwareModule.getType().getKey()); + response.setVendor(baseSofwareModule.getVendor()); + + response.add(linkTo(methodOn(SoftwareModuleResource.class).getArtifacts(response.getModuleId())) + .withRel(RestConstants.SOFTWAREMODULE_V1_ARTIFACT)); + response.add(linkTo(methodOn(SoftwareModuleResource.class).getSoftwareModule(response.getModuleId())) + .withRel("self")); + + response.add(linkTo( + methodOn(SoftwareModuleTypeResource.class).getSoftwareModuleType(baseSofwareModule.getType().getId())) + .withRel(RestConstants.SOFTWAREMODULE_V1_TYPE)); + + return response; + } + + /** + * @param artifact + * @return + */ + static ArtifactRest toResponse(final Artifact artifact) { + final ArtifactRest.ArtifactType type = artifact instanceof LocalArtifact ? ArtifactRest.ArtifactType.LOCAL + : ArtifactRest.ArtifactType.EXTERNAL; + + final ArtifactRest artifactRest = new ArtifactRest(); + artifactRest.setType(type); + artifactRest.setArtifactId(artifact.getId()); + artifactRest.setSize(artifact.getSize()); + artifactRest.setHashes(new ArtifactHash(artifact.getSha1Hash(), artifact.getMd5Hash())); + + if (artifact instanceof LocalArtifact) { + artifactRest.setProvidedFilename(((LocalArtifact) artifact).getFilename()); + } + + RestModelMapper.mapBaseToBase(artifactRest, artifact); + + artifactRest.add(linkTo(methodOn(SoftwareModuleResource.class).getArtifact(artifact.getSoftwareModule().getId(), + artifact.getId())).withRel("self")); + + if (artifact instanceof LocalArtifact) { + artifactRest.add( + linkTo(methodOn(SoftwareModuleResource.class).downloadArtifact(artifact.getSoftwareModule().getId(), + artifact.getId(), null, null)).withRel("download")); + } + + return artifactRest; + } + + static ArtifactsRest artifactsToResponse(final List artifacts) { + final ArtifactsRest mappedList = new ArtifactsRest(); + + if (artifacts != null) { + for (final Artifact artifact : artifacts) { + final ArtifactRest response = toResponse(artifact); + mappedList.add(response); + } + } + return mappedList; + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleResource.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleResource.java new file mode 100755 index 000000000..a56a733c7 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleResource.java @@ -0,0 +1,603 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; + +import java.io.IOException; +import java.util.List; + +import javax.persistence.EntityManager; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.repository.ArtifactManagement; +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.repository.SoftwareModuleFields; +import org.eclipse.hawkbit.repository.SoftwareModuleMetadataFields; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.Artifact; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata; +import org.eclipse.hawkbit.repository.model.SwMetadataCompositeKey; +import org.eclipse.hawkbit.repository.rsql.RSQLUtility; +import org.eclipse.hawkbit.rest.resource.helper.RestResourceConversionHelper; +import org.eclipse.hawkbit.rest.resource.model.ExceptionInfo; +import org.eclipse.hawkbit.rest.resource.model.MetadataRest; +import org.eclipse.hawkbit.rest.resource.model.MetadataRestPageList; +import org.eclipse.hawkbit.rest.resource.model.artifact.ArtifactRest; +import org.eclipse.hawkbit.rest.resource.model.artifact.ArtifactsRest; +import org.eclipse.hawkbit.rest.resource.model.softwaremodule.SoftwareModulePagedList; +import org.eclipse.hawkbit.rest.resource.model.softwaremodule.SoftwareModuleRequestBodyPost; +import org.eclipse.hawkbit.rest.resource.model.softwaremodule.SoftwareModuleRequestBodyPut; +import org.eclipse.hawkbit.rest.resource.model.softwaremodule.SoftwareModuleRest; +import org.eclipse.hawkbit.rest.resource.model.softwaremodule.SoftwareModulesRest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +/** + * REST Resource handling for {@link SoftwareModule} and related + * {@link Artifact} CRUD operations. + * + * + * + * + */ +@RestController +@Transactional(readOnly = true) +@RequestMapping(RestConstants.SOFTWAREMODULE_V1_REQUEST_MAPPING) +@Api(value = "softwaremodules", description = "Software Modules Management API") +public class SoftwareModuleResource { + private static final Logger LOG = LoggerFactory.getLogger(SoftwareModuleResource.class); + + @Autowired + private ArtifactManagement artifactManagement; + + @Autowired + private SoftwareManagement softwareManagement; + + @Autowired + private EntityManager entityManager; + + /** + * Handles POST request for artifact upload. + * + * @param softwareModuleId + * of the parent {@link SoftwareModule} + * @param file + * that has to be uploaded + * @param optionalFileName + * to override {@link MultipartFile#getOriginalFilename()} + * @param md5Sum + * checksum for uploaded content check + * @param sha1Sum + * checksum for uploaded content check + * + * @return {@link ResponseEntity} if status {@link HttpStatus#CREATED} if + * successful + */ + @RequestMapping(method = RequestMethod.POST, value = "/{softwareModuleId}/artifacts", produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = ArtifactRest.class, value = "Upload Artifact", notes = "Handles POST request for artifact upload. Required Permission: " + + SpPermission.CREATE_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Software Module", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity uploadArtifact(@PathVariable final Long softwareModuleId, + @RequestParam("file") final MultipartFile file, + @RequestParam(value = "filename", required = false) final String optionalFileName, + @RequestParam(value = "md5sum", required = false) final String md5Sum, + @RequestParam(value = "sha1sum", required = false) final String sha1Sum) { + + Artifact result = null; + if (!file.isEmpty()) { + String fileName = optionalFileName; + + if (null == fileName) { + fileName = file.getOriginalFilename(); + } + + try { + result = artifactManagement.createLocalArtifact(file.getInputStream(), softwareModuleId, fileName, + md5Sum == null ? null : md5Sum.toLowerCase(), sha1Sum == null ? null : sha1Sum.toLowerCase(), + false, file.getContentType()); + } catch (final IOException e) { + LOG.error("Failed to store artifact", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } else { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + // we flush to ensure that entity is generated and we can return ID etc. + entityManager.flush(); + + return new ResponseEntity(SoftwareModuleMapper.toResponse(result), HttpStatus.CREATED); + + } + + /** + * Handles the GET request of retrieving all meta data of artifacts assigned + * to a software module. + * + * @param softwareModuleId + * of the parent {@link SoftwareModule} + * + * @return {@link ResponseEntity} with status {@link HttpStatus#OK} if + * successful + */ + @RequestMapping(method = RequestMethod.GET, value = "/{softwareModuleId}/artifacts", produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = ArtifactsRest.class, value = "List Artifacts Metadata", notes = "Handles the GET request of retrieving all meta data of artifacts assigned to a software module. Required Permission: " + + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Software Module", response = ExceptionInfo.class)) + @ResponseBody + public ResponseEntity getArtifacts(@PathVariable final Long softwareModuleId) { + final SoftwareModule module = findSoftwareModuleWithExceptionIfNotFound(softwareModuleId, null); + + return new ResponseEntity(SoftwareModuleMapper.artifactsToResponse(module.getArtifacts()), + HttpStatus.OK); + } + + /** + * Handles the GET request for downloading an artifact. + * + * @param softwareModuleId + * of the parent {@link SoftwareModule} + * @param artifactId + * of the related {@link LocalArtifact} + * @param servletResponse + * of the servlet + * @param request + * of the client + * + * @return {@link ResponseEntity} with status {@link HttpStatus#OK} if + * successful + */ + @RequestMapping(method = RequestMethod.GET, value = "/{softwareModuleId}/artifacts/{artifactId}/download") + @ApiOperation(response = Void.class, value = "Download Artifact", notes = "Handles the GET request for downloading an artifact. Required Permission: " + + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Software Module or Artifact", response = ExceptionInfo.class)) + @ResponseBody + public ResponseEntity downloadArtifact(@PathVariable final Long softwareModuleId, + @PathVariable final Long artifactId, final HttpServletResponse servletResponse, + final HttpServletRequest request) { + final SoftwareModule module = findSoftwareModuleWithExceptionIfNotFound(softwareModuleId, artifactId); + + if (null == module || !module.getLocalArtifact(artifactId).isPresent()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + final LocalArtifact artifact = module.getLocalArtifact(artifactId).get(); + final DbArtifact file = artifactManagement.loadLocalArtifactBinary(artifact); + + final String ifMatch = request.getHeader("If-Match"); + if (ifMatch != null && !RestResourceConversionHelper.matchesHttpHeader(ifMatch, artifact.getSha1Hash())) { + return new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED); + } + + return RestResourceConversionHelper.writeFileResponse(artifact, servletResponse, request, file); + + } + + /** + * Handles the GET request of retrieving a single Artifact meta data + * request. + * + * @param softwareModuleId + * of the parent {@link SoftwareModule} + * @param artifactId + * of the related {@link LocalArtifact} + * + * @return {@link ResponseEntity} with status {@link HttpStatus#OK} if + * successful + */ + @RequestMapping(method = RequestMethod.GET, value = "/{softwareModuleId}/artifacts/{artifactId}", produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = ArtifactRest.class, value = "Get Artifact Metadata", notes = "Handles the GET request of retrieving a single Artifact meta data request. Required Permission: " + + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Software Module or Artifact", response = ExceptionInfo.class)) + @ResponseBody + public ResponseEntity getArtifact(@PathVariable final Long softwareModuleId, + @PathVariable final Long artifactId) { + final SoftwareModule module = findSoftwareModuleWithExceptionIfNotFound(softwareModuleId, artifactId); + + return new ResponseEntity(SoftwareModuleMapper.toResponse(module.getLocalArtifact(artifactId) + .get()), HttpStatus.OK); + } + + /** + * Handles the DELETE request for a single SoftwareModule within SP. + * + * @param softwareModuleId + * the ID of the module that has the artifact + * @param artifactId + * of the artifact to be deleted + * + * @return status OK if delete as successful. + */ + @RequestMapping(method = RequestMethod.DELETE, value = "/{softwareModuleId}/artifacts/{artifactId}") + @ApiOperation(value = "Delete Artifact", notes = "Handles the DELETE request for a single SoftwareModule within SP. Required Permission: " + + SpPermission.DELETE_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Software Module or Artifact", response = ExceptionInfo.class)) + @ResponseBody + @Transactional + public ResponseEntity deleteArtifact(@PathVariable final Long softwareModuleId, + @PathVariable final Long artifactId) { + findSoftwareModuleWithExceptionIfNotFound(softwareModuleId, artifactId); + + artifactManagement.deleteLocalArtifact(artifactId); + + return new ResponseEntity<>(HttpStatus.OK); + + } + + /** + * Handles the GET request of retrieving all softwaremodules within SP. + * + * @param pagingOffsetParam + * the offset of list of modules for pagination, might not be + * present in the rest request then default value will be applied + * @param pagingLimitParam + * the limit of the paged request, might not be present in the + * rest request then default value will be applied + * @param sortParam + * the sorting parameter in the request URL, syntax + * {@code field:direction, field:direction} + * @param rsqlParam + * the search parameter in the request URL, syntax + * {@code q=name==abc} + * + * @return a list of all modules for a defined or default page request with + * status OK. The response is always paged. In any failure the + * JsonResponseExceptionHandler is handling the response. + */ + @RequestMapping(method = RequestMethod.GET, produces = { "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = SoftwareModulePagedList.class, value = "Get Software Modules", notes = "Handles the GET request of retrieving all softwaremodules within SP. Required Permission: " + + SpPermission.READ_REPOSITORY) + public ResponseEntity getSoftwareModules( + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam, + @ApiParam(required = false, value = "FIQL syntax search query" + + "" + + "" + + "" + + "" + + "" + "
version==1.0.0softwaremodules with version 1.0.0
version=ge=2.0.0softwaremodules with version greater-equal 2.0.0
version==1.0.0;type=applicationsoftwaremodules with version 1.0.0 and type application
id=1,type=ossoftwaremodules with id 1 or type os
") @RequestParam(value = RestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam) { + + final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); + final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); + final Sort sorting = PagingUtility.sanitizeSoftwareModuleSortParam(sortParam); + + final Pageable pageable = new OffsetBasedPageRequest(sanitizedOffsetParam, sanitizedLimitParam, sorting); + + final Slice findModulesAll; + Long countModulesAll; + if (rsqlParam != null) { + findModulesAll = softwareManagement.findSoftwareModulesByPredicate( + RSQLUtility.parse(rsqlParam, SoftwareModuleFields.class, entityManager), pageable); + countModulesAll = ((Page) findModulesAll).getTotalElements(); + } else { + findModulesAll = softwareManagement.findSoftwareModulesAll(pageable); + countModulesAll = softwareManagement.countSoftwareModulesAll(); + } + + final List rest = SoftwareModuleMapper.toResponse(findModulesAll.getContent()); + return new ResponseEntity<>(new SoftwareModulePagedList(rest, countModulesAll), HttpStatus.OK); + } + + /** + * Handles the GET request of retrieving a single software module within SP. + * + * @param softwareModuleId + * the ID of the module to retrieve + * + * @return a single softwareModule with status OK. + * @throws EntityNotFoundException + * in case no with the given {@code softwareModuleId} exists. + */ + @RequestMapping(method = RequestMethod.GET, value = "/{softwareModuleId}", produces = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = SoftwareModuleRest.class, value = "Get Software Module", notes = "Handles the GET request of retrieving a single softwaremodule within SP. Required Permission: " + + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Software Module", response = ExceptionInfo.class)) + public ResponseEntity getSoftwareModule(@PathVariable final Long softwareModuleId) { + final SoftwareModule findBaseSoftareModule = findSoftwareModuleWithExceptionIfNotFound(softwareModuleId, null); + + return new ResponseEntity<>(SoftwareModuleMapper.toResponse(findBaseSoftareModule), HttpStatus.OK); + } + + /** + * Handles the POST request of creating new softwaremodules within SP. The + * request body must always be a list of modules. The requests is delgating + * to the {@link SoftwareManagement#createSoftwareModule(Iterable)}. + * + * @param softwareModules + * the modules to be created. + * @return In case all modules could successful created the ResponseEntity + * with status code 201 - Created but without ResponseBody. In any + * failure the JsonResponseExceptionHandler is handling the + * response. + */ + @RequestMapping(method = RequestMethod.POST, consumes = { "application/hal+json", MediaType.APPLICATION_JSON_VALUE }, produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = SoftwareModulesRest.class, value = "Create Software Modules", notes = "Handles the POST request of creating new software modules within SP. The request body must always be a list of modules. Required Permission: " + + SpPermission.CREATE_REPOSITORY) + @ApiResponses({ + @ApiResponse(code = 404, message = "Not Found Software Module", response = ExceptionInfo.class), + @ApiResponse(code = 409, message = "Conflict Software Module already exists", response = ExceptionInfo.class) }) + @Transactional + public ResponseEntity createSoftwareModules( + @RequestBody final List softwareModules) { + LOG.debug("creating {} softwareModules", softwareModules.size()); + final Iterable createdSoftwareModules = softwareManagement + .createSoftwareModule(SoftwareModuleMapper.smFromRequest(softwareModules, softwareManagement)); + LOG.debug("{} softwareModules created, return status {}", softwareModules.size(), HttpStatus.CREATED); + + // we flush to ensure that entity is generated and we can return ID etc. + entityManager.flush(); + + return new ResponseEntity<>(SoftwareModuleMapper.toResponseSoftwareModules(createdSoftwareModules), + HttpStatus.CREATED); + } + + /** + * Handles the PUT request of updating a software module within SP. + * {@link SoftwareManagement#createSoftwareModule(Iterable)}. + * + * @param softwareModuleId + * the ID of the software module in the URL + * @param restSoftwareModule + * the modules to be updated. + * @return status OK if update is successful + */ + @RequestMapping(method = RequestMethod.PUT, value = "/{softwareModuleId}", consumes = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }, produces = { "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(value = "Update Software Module", notes = "Handles the PUT request for a single softwaremodule within SP. Required Permission: " + + SpPermission.UPDATE_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Software Module", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity updateSoftwareModule(@PathVariable final Long softwareModuleId, + @RequestBody final SoftwareModuleRequestBodyPut restSoftwareModule) { + final SoftwareModule module = findSoftwareModuleWithExceptionIfNotFound(softwareModuleId, null); + + // only description and vendor can be modified + if (restSoftwareModule.getDescription() != null) { + module.setDescription(restSoftwareModule.getDescription()); + } + if (restSoftwareModule.getVendor() != null) { + module.setVendor(restSoftwareModule.getVendor()); + } + + final SoftwareModule updateSoftwareModule = softwareManagement.updateSoftwareModule(module); + + // we flush to ensure that entity is generated and we can return ID etc. + entityManager.flush(); + + return new ResponseEntity<>(SoftwareModuleMapper.toResponse(updateSoftwareModule), HttpStatus.OK); + } + + /** + * Handles the DELETE request for a single softwaremodule within SP. + * + * @param softwareModuleId + * the ID of the module to retrieve + * @return status OK if delete as sucessfull. + * + */ + @RequestMapping(method = RequestMethod.DELETE, value = "/{softwareModuleId}") + @ApiOperation(value = "Delete Software Module", notes = "Handles the DELETE request for a single softwaremodule within SP. Required Permission: " + + SpPermission.DELETE_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Software Module", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity deleteSoftwareModule(@PathVariable final Long softwareModuleId) { + final SoftwareModule module = findSoftwareModuleWithExceptionIfNotFound(softwareModuleId, null); + + softwareManagement.deleteSoftwareModule(module); + + return new ResponseEntity<>(HttpStatus.OK); + } + + /** + * Gets a paged list of meta data for a software module. + * + * @param softwareModuleId + * the ID of the software module for the meta data + * @param pagingOffsetParam + * the offset of list of meta data for pagination, might not be + * present in the rest request then default value will be applied + * @param pagingLimitParam + * the limit of the paged request, might not be present in the + * rest request then default value will be applied + * @param sortParam + * the sorting parameter in the request URL, syntax + * {@code field:direction, field:direction} + * @param rsqlParam + * the search parameter in the request URL, syntax + * {@code q=key==abc} + * @return status OK if get request is successful with the paged list of + * meta data + */ + @RequestMapping(method = RequestMethod.GET, value = "/{softwareModuleId}/metadata", produces = { + MediaType.APPLICATION_JSON_VALUE, "application/hal+json" }) + @ApiOperation(response = MetadataRestPageList.class, value = "Get a paged list of meta data", notes = " Get a paged list of meta data for a software module." + + " Required Permission: " + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Software Module", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity getMetadata( + @PathVariable final Long softwareModuleId, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam, + @ApiParam(required = false, value = "FIQL syntax search query" + + "" + + "" + + "" + + "
key==aKey,key==bKeymetadata with the key 'aKey' or 'bKey'
value=li=%25someValue%25metadata which contains 'someValue' in the value ignore case
") @RequestParam(value = RestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam) { + + // check if software module exists otherwise throw exception immediately + findSoftwareModuleWithExceptionIfNotFound(softwareModuleId, null); + + final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); + final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); + final Sort sorting = PagingUtility.sanitizeSoftwareModuleMetadataSortParam(sortParam); + + final Pageable pageable = new OffsetBasedPageRequest(sanitizedOffsetParam, sanitizedLimitParam, sorting); + final Page metaDataPage; + + if (rsqlParam != null) { + metaDataPage = softwareManagement.findSoftwareModuleMetadataBySoftwareModuleId(softwareModuleId, + RSQLUtility.parse(rsqlParam, SoftwareModuleMetadataFields.class, entityManager), pageable); + } else { + metaDataPage = softwareManagement.findSoftwareModuleMetadataBySoftwareModuleId(softwareModuleId, pageable); + } + + return new ResponseEntity<>(new MetadataRestPageList(SoftwareModuleMapper.toResponseSwMetadata(metaDataPage + .getContent()), metaDataPage.getTotalElements()), HttpStatus.OK); + } + + /** + * Gets a single meta data value for a specific key of a software module. + * + * @param softwareModuleId + * the ID of the software module to get the meta data from + * @param metadataKey + * the key of the meta data entry to retrieve the value from + * @return status OK if get request is successful with the value of the meta + * data + */ + @RequestMapping(method = RequestMethod.GET, value = "/{softwareModuleId}/metadata/{metadataKey}", produces = { MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = MetadataRest.class, value = "Get a single meta data value", notes = " Get a single meta data value for a meta data key." + + " Required Permission: " + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Software Module or meta data key", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity getMetadataValue(@PathVariable final Long softwareModuleId, + @PathVariable final String metadataKey) { + // check if distribution set exists otherwise throw exception + // immediately + final SoftwareModule sw = findSoftwareModuleWithExceptionIfNotFound(softwareModuleId, null); + final SoftwareModuleMetadata findOne = softwareManagement.findOne(new SwMetadataCompositeKey(sw, metadataKey)); + return ResponseEntity. ok(SoftwareModuleMapper.toResponseSwMetadata(findOne)); + } + + /** + * Updates a single meta data value of a software module. + * + * @param softwareModuleId + * the ID of the software module to update the meta data entry + * @param metadataKey + * the key of the meta data to update the value + * @return status OK if the update request is successful and the updated + * meta data result + */ + @RequestMapping(method = RequestMethod.PUT, value = "/{softwareModuleId}/metadata/{metadataKey}", produces = { + MediaType.APPLICATION_JSON_VALUE, "application/hal+json" }) + @ApiOperation(response = MetadataRest.class, value = "Update a single meta data value", notes = " Update a single meta data value for speficic key." + + " Required Permission: " + SpPermission.UPDATE_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Software Module or meta data key", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity updateMetadata(@PathVariable final Long softwareModuleId, + @PathVariable final String metadataKey, @RequestBody final MetadataRest metadata) { + // check if software module exists otherwise throw exception immediately + final SoftwareModule sw = findSoftwareModuleWithExceptionIfNotFound(softwareModuleId, null); + final SoftwareModuleMetadata updated = softwareManagement + .updateSoftwareModuleMetadata(new SoftwareModuleMetadata(metadataKey, sw, metadata.getValue())); + return ResponseEntity.ok(SoftwareModuleMapper.toResponseSwMetadata(updated)); + } + + /** + * Deletes a single meta data entry from the software module. + * + * @param softwareModuleId + * the ID of the software module to delete the meta data entry + * @param metadataKey + * the key of the meta data to delete + * @return status OK if the delete request is successful + */ + @RequestMapping(method = RequestMethod.DELETE, value = "/{softwareModuleId}/metadata/{metadataKey}") + @ApiOperation(value = "Delete a single meta data", notes = " Delete a single meta data." + " Required Permission: " + + SpPermission.UPDATE_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Software Module or meta data key", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity deleteMetadata(@PathVariable final Long softwareModuleId, + @PathVariable final String metadataKey) { + // check if software module exists otherwise throw exception immediately + final SoftwareModule sw = findSoftwareModuleWithExceptionIfNotFound(softwareModuleId, null); + softwareManagement.deleteSoftwareModuleMetadata(new SwMetadataCompositeKey(sw, metadataKey)); + return ResponseEntity.ok().build(); + } + + /** + * Creates a list of meta data for a specific software module. + * + * @param softwareModuleId + * the ID of the distribution set to create meta data for + * @param metadataRest + * the list of meta data entries to create + * @return status created if post request is successful with the value of + * the created meta data + */ + @RequestMapping(method = RequestMethod.POST, value = "/{softwareModuleId}/metadata", consumes = { + MediaType.APPLICATION_JSON_VALUE, "application/hal+json" }, produces = { MediaType.APPLICATION_JSON_VALUE, + "application/hal+json" }) + @ApiOperation(response = MetadataRest.class, value = "Create a list of meta data entries", notes = "Create a list of meta data entries" + + " Required Permission: " + SpPermission.READ_REPOSITORY + " and " + SpPermission.UPDATE_TARGET) + @ApiResponses({ @ApiResponse(code = 409, message = "Conflict", response = ExceptionInfo.class), + @ApiResponse(code = 404, message = "Not Found Distribution Set", response = ExceptionInfo.class) }) + @Transactional + public ResponseEntity> createMetadata(@PathVariable final Long softwareModuleId, + @RequestBody final List metadataRest) { + // check if software module exists otherwise throw exception immediately + final SoftwareModule sw = findSoftwareModuleWithExceptionIfNotFound(softwareModuleId, null); + + final List created = softwareManagement + .createSoftwareModuleMetadata(SoftwareModuleMapper.fromRequestSwMetadata(sw, metadataRest)); + + // we flush to ensure that entity is generated and we can return ID etc. + entityManager.flush(); + + return new ResponseEntity>(SoftwareModuleMapper.toResponseSwMetadata(created), + HttpStatus.CREATED); + + } + + private SoftwareModule findSoftwareModuleWithExceptionIfNotFound(final Long softwareModuleId, final Long artifactId) { + final SoftwareModule module = softwareManagement.findSoftwareModuleById(softwareModuleId); + if (module == null) { + throw new EntityNotFoundException("SoftwareModule with Id {" + softwareModuleId + "} does not exist"); + } else if (artifactId != null && !module.getLocalArtifact(artifactId).isPresent()) { + throw new EntityNotFoundException("Artifact with Id {" + artifactId + "} does not exist"); + } + return module; + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleTypeMapper.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleTypeMapper.java new file mode 100755 index 000000000..d187aa63a --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleTypeMapper.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.rest.resource.model.softwaremoduletype.SoftwareModuleTypeRequestBodyPost; +import org.eclipse.hawkbit.rest.resource.model.softwaremoduletype.SoftwareModuleTypeRest; +import org.eclipse.hawkbit.rest.resource.model.softwaremoduletype.SoftwareModuleTypesRest; + +/** + * A mapper which maps repository model to RESTful model representation and + * back. + * + * + * + * + */ +final class SoftwareModuleTypeMapper { + + // private constructor, utility class + private SoftwareModuleTypeMapper() { + + } + + static List smFromRequest(final Iterable smTypesRest) { + final List mappedList = new ArrayList<>(); + + for (final SoftwareModuleTypeRequestBodyPost smRest : smTypesRest) { + mappedList.add(fromRequest(smRest)); + } + return mappedList; + } + + static SoftwareModuleType fromRequest(final SoftwareModuleTypeRequestBodyPost smsRest) { + return new SoftwareModuleType(smsRest.getKey(), smsRest.getName(), smsRest.getDescription(), + smsRest.getMaxAssignments()); + } + + static SoftwareModuleTypesRest toTypesResponse(final List types) { + final SoftwareModuleTypesRest response = new SoftwareModuleTypesRest(); + for (final SoftwareModuleType softwareModule : types) { + response.add(toResponse(softwareModule)); + } + return response; + } + + static List toListResponse(final Collection types) { + final List response = new ArrayList(); + for (final SoftwareModuleType softwareModule : types) { + response.add(toResponse(softwareModule)); + } + return response; + } + + static SoftwareModuleTypeRest toResponse(final SoftwareModuleType type) { + final SoftwareModuleTypeRest result = new SoftwareModuleTypeRest(); + + RestModelMapper.mapNamedToNamed(result, type); + result.setKey(type.getKey()); + result.setMaxAssignments(type.getMaxAssignments()); + result.setModuleId(type.getId()); + + result.add(linkTo(methodOn(SoftwareModuleTypeResource.class).getSoftwareModuleType(result.getModuleId())) + .withRel("self")); + + return result; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleTypeResource.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleTypeResource.java new file mode 100755 index 000000000..c023c9b3c --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleTypeResource.java @@ -0,0 +1,242 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; + +import java.util.List; + +import javax.persistence.EntityManager; + +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.repository.SoftwareModuleTypeFields; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.Artifact; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.repository.rsql.RSQLUtility; +import org.eclipse.hawkbit.rest.resource.model.ExceptionInfo; +import org.eclipse.hawkbit.rest.resource.model.softwaremoduletype.SoftwareModuleTypePagedList; +import org.eclipse.hawkbit.rest.resource.model.softwaremoduletype.SoftwareModuleTypeRequestBodyPost; +import org.eclipse.hawkbit.rest.resource.model.softwaremoduletype.SoftwareModuleTypeRequestBodyPut; +import org.eclipse.hawkbit.rest.resource.model.softwaremoduletype.SoftwareModuleTypeRest; +import org.eclipse.hawkbit.rest.resource.model.softwaremoduletype.SoftwareModuleTypesRest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * REST Resource handling for {@link SoftwareModule} and related + * {@link Artifact} CRUD operations. + * + * + * + * + */ +@RestController +@Transactional(readOnly = true) +@RequestMapping(RestConstants.SOFTWAREMODULETYPE_V1_REQUEST_MAPPING) +@Api(value = "softwaremoduletypes", description = "Software Module Types Management API") +public class SoftwareModuleTypeResource { + @Autowired + private SoftwareManagement softwareManagement; + + @Autowired + private EntityManager entityManager; + + /** + * Handles the GET request of retrieving all {@link SoftwareModuleType}s + * within SP. + * + * @param pagingOffsetParam + * the offset of list of modules for pagination, might not be + * present in the rest request then default value will be applied + * @param pagingLimitParam + * the limit of the paged request, might not be present in the + * rest request then default value will be applied + * @param sortParam + * the sorting parameter in the request URL, syntax + * {@code field:direction, field:direction} + * @param rsqlParam + * the search parameter in the request URL, syntax + * {@code q=name==abc} + * + * @return a list of all module type for a defined or default page request + * with status OK. The response is always paged. In any failure the + * JsonResponseExceptionHandler is handling the response. + */ + @RequestMapping(method = RequestMethod.GET, produces = { "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = SoftwareModuleTypePagedList.class, value = "Get Software Module Types", notes = "Handles the GET request of retrieving all software module types within SP. Required Permission: " + + SpPermission.READ_REPOSITORY) + public ResponseEntity getTypes( + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam, + @ApiParam(required = false, value = "FIQL syntax search query" + "" + + "" + + "" + + "
id=1,key=ossoftware module types with id 1 or key os
name=Applicationsoftware module types with name Application
") @RequestParam(value = RestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam) { + + final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); + final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); + final Sort sorting = PagingUtility.sanitizeSoftwareModuleSortParam(sortParam); + + final Pageable pageable = new OffsetBasedPageRequest(sanitizedOffsetParam, sanitizedLimitParam, sorting); + + final Slice findModuleTypessAll; + Long countModulesAll; + if (rsqlParam != null) { + findModuleTypessAll = softwareManagement.findSoftwareModuleTypesByPredicate( + RSQLUtility.parse(rsqlParam, SoftwareModuleTypeFields.class, entityManager), pageable); + countModulesAll = ((Page) findModuleTypessAll).getTotalElements(); + } else { + findModuleTypessAll = softwareManagement.findSoftwareModuleTypesAll(pageable); + countModulesAll = softwareManagement.countSoftwareModuleTypesAll(); + } + + final List rest = SoftwareModuleTypeMapper.toListResponse(findModuleTypessAll + .getContent()); + return new ResponseEntity<>(new SoftwareModuleTypePagedList(rest, countModulesAll), HttpStatus.OK); + } + + /** + * Handles the GET request of retrieving a single software module type + * within SP. + * + * @param softwareModuleTypeId + * the ID of the module type to retrieve + * + * @return a single softwareModule with status OK. + * @throws EntityNotFoundException + * in case no with the given {@code softwareModuleId} exists. + */ + @RequestMapping(method = RequestMethod.GET, value = "/{softwareModuleTypeId}", produces = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = SoftwareModuleTypeRest.class, value = "Get Software Module Type", notes = "Handles the GET request of retrieving a single software module type within SP. Required Permission: " + + SpPermission.READ_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Software Module Type", response = ExceptionInfo.class)) + public ResponseEntity getSoftwareModuleType(@PathVariable final Long softwareModuleTypeId) { + final SoftwareModuleType foundType = findSoftwareModuleTypeWithExceptionIfNotFound(softwareModuleTypeId); + + return new ResponseEntity<>(SoftwareModuleTypeMapper.toResponse(foundType), HttpStatus.OK); + } + + /** + * Handles the DELETE request for a single software module type within SP. + * + * @param softwareModuleTypeId + * the ID of the module to retrieve + * @return status OK if delete as sucessfull. + * + */ + @RequestMapping(method = RequestMethod.DELETE, value = "/{softwareModuleTypeId}") + @ApiOperation(value = "Delete Software Module Type", notes = "Handles the DELETE request for a single software module Type within SP. Required Permission: " + + SpPermission.DELETE_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Software Module Type", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity deleteSoftwareModuleType(@PathVariable final Long softwareModuleTypeId) { + final SoftwareModuleType module = findSoftwareModuleTypeWithExceptionIfNotFound(softwareModuleTypeId); + + softwareManagement.deleteSoftwareModuleType(module); + + return new ResponseEntity<>(HttpStatus.OK); + } + + /** + * Handles the PUT request of updating a software module type within SP. + * + * @param softwareModuleTypeId + * the ID of the software module in the URL + * @param restSoftwareModuleType + * the module type to be updated. + * @return status OK if update is successful + */ + @RequestMapping(method = RequestMethod.PUT, value = "/{softwareModuleTypeId}", consumes = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }, produces = { "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(value = "Updates Software Module Type", notes = "Handles the PUT request for a single software module type within SP. Required Permission: " + + SpPermission.UPDATE_REPOSITORY) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Software Module", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity updateSoftwareModuleType( + @PathVariable final Long softwareModuleTypeId, + @RequestBody final SoftwareModuleTypeRequestBodyPut restSoftwareModuleType) { + final SoftwareModuleType type = findSoftwareModuleTypeWithExceptionIfNotFound(softwareModuleTypeId); + + // only description can be modified + if (restSoftwareModuleType.getDescription() != null) { + type.setDescription(restSoftwareModuleType.getDescription()); + } + + final SoftwareModuleType updatedSoftwareModuleType = softwareManagement.updateSoftwareModuleType(type); + + // we flush to ensure that entity is generated and we can return ID etc. + entityManager.flush(); + + return new ResponseEntity<>(SoftwareModuleTypeMapper.toResponse(updatedSoftwareModuleType), HttpStatus.OK); + } + + /** + * Handles the POST request of creating new {@link SoftwareModuleType}s + * within SP. The request body must always be a list of types. + * + * @param softwareModuleTypes + * the modules to be created. + * @return In case all modules could successful created the ResponseEntity + * with status code 201 - Created but without ResponseBody. In any + * failure the JsonResponseExceptionHandler is handling the + * response. + */ + @RequestMapping(method = RequestMethod.POST, consumes = { "application/hal+json", MediaType.APPLICATION_JSON_VALUE }, produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = SoftwareModuleTypesRest.class, value = "Create Software Module Types", notes = "Handles the POST request of creating new software module types within SP. The request body must always be a list of module types. Required Permission: " + + SpPermission.CREATE_REPOSITORY) + @ApiResponses({ + @ApiResponse(code = 404, message = "Not Found Software Module Type", response = ExceptionInfo.class), + @ApiResponse(code = 409, message = "Conflict Software Module Type already exists", response = ExceptionInfo.class) }) + @Transactional + public ResponseEntity createSoftwareModuleTypes( + @RequestBody final List softwareModuleTypes) { + + final List createdSoftwareModules = softwareManagement + .createSoftwareModuleTypes(SoftwareModuleTypeMapper.smFromRequest(softwareModuleTypes)); + + // we flush to ensure that entity is generated and we can return ID etc. + entityManager.flush(); + + return new ResponseEntity<>(SoftwareModuleTypeMapper.toTypesResponse(createdSoftwareModules), + HttpStatus.CREATED); + } + + private SoftwareModuleType findSoftwareModuleTypeWithExceptionIfNotFound(final Long softwareModuleTypeId) { + final SoftwareModuleType module = softwareManagement.findSoftwareModuleTypeById(softwareModuleTypeId); + if (module == null) { + throw new EntityNotFoundException("SoftwareModuleType with Id {" + softwareModuleTypeId + + "} does not exist"); + } + return module; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SortDirection.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SortDirection.java new file mode 100755 index 000000000..344c27ce7 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SortDirection.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +/** + * A definition of possible sorting direction. + * + * + * + */ +public enum SortDirection { + /** + * Ascending. + */ + ASC, + /** + * Descending. + */ + DESC; + + /** + * Returns the sort direction for the given name. + * + * @param name + * the name of the enum + * @return the corresponding enum + * @throws SortParameterUnsupportedDirectionException + * if there is no matching enum for the specified name + */ + public static SortDirection getByName(final String name) { + try { + return valueOf(name.toUpperCase()); + } catch (final IllegalArgumentException ex) {// NOSONAR + throw new SortParameterUnsupportedDirectionException(); + } + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SortParameterSyntaxErrorException.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SortParameterSyntaxErrorException.java new file mode 100755 index 000000000..b6bacd9d2 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SortParameterSyntaxErrorException.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * Exception used by the REST API in case of invalid sort parameter syntax. + * + * + * + * + */ +public class SortParameterSyntaxErrorException extends SpServerRtException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new SortParameterSyntaxErrorException with + * {@link SpServerError#SP_REST_SORT_PARAM_SYNTAX} error. + */ + public SortParameterSyntaxErrorException() { + super(SpServerError.SP_REST_SORT_PARAM_SYNTAX); + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SortParameterUnsupportedDirectionException.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SortParameterUnsupportedDirectionException.java new file mode 100755 index 000000000..60fa3d8df --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SortParameterUnsupportedDirectionException.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * Exception used by the REST API in case of invalid sort parameter direction + * name. + * + * + * + * + */ +public class SortParameterUnsupportedDirectionException extends SpServerRtException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new SortParameterSyntaxErrorException. + */ + public SortParameterUnsupportedDirectionException() { + super(SpServerError.SP_REST_SORT_PARAM_INVALID_DIRECTION); + } + + /** + * Creates a new SortParameterSyntaxErrorException with + * {@link SpServerError#SP_REST_SORT_PARAM_INVALID_DIRECTION} error. + * + * @param cause + * the cause (which is saved for later retrieval by the + * getCause() method). (A null value is permitted, and indicates + * that the cause is nonexistent or unknown.) + */ + public SortParameterUnsupportedDirectionException(final Throwable cause) { + super(SpServerError.SP_REST_SORT_PARAM_INVALID_DIRECTION, cause); + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SortParameterUnsupportedFieldException.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SortParameterUnsupportedFieldException.java new file mode 100755 index 000000000..20c6f03d6 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SortParameterUnsupportedFieldException.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.exception.SpServerRtException; + +/** + * Exception used by the REST API in case of invalid field name in the sort + * parameter. + * + * + * + * + */ +public class SortParameterUnsupportedFieldException extends SpServerRtException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new SortParameterSyntaxErrorException with + * {@link SpServerError#SP_REST_SORT_PARAM_INVALID_FIELD} error. + */ + public SortParameterUnsupportedFieldException() { + super(SpServerError.SP_REST_SORT_PARAM_INVALID_FIELD); + } + + /** + * Creates a new SortParameterSyntaxErrorException with + * {@link SpServerError#SP_REST_SORT_PARAM_INVALID_FIELD} error. + * + * @param cause + * the cause (which is saved for later retrieval by the + * getCause() method). (A null value is permitted, and indicates + * that the cause is nonexistent or unknown.) + */ + public SortParameterUnsupportedFieldException(final Throwable cause) { + super(SpServerError.SP_REST_SORT_PARAM_INVALID_FIELD, cause); + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SortUtility.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SortUtility.java new file mode 100755 index 000000000..f8b997bf9 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SortUtility.java @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import java.util.List; +import java.util.StringTokenizer; + +import org.eclipse.hawkbit.repository.FieldNameProvider; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.domain.Sort.Order; + +import com.google.common.collect.Lists; + +/** + * A utility class for parsing query parameters which define the sorting of + * elements. + * + * + * + */ +public final class SortUtility { + + /** + * the delimiter between the field and direction in the sort request. + */ + public static final String DELIMITER_FIELD_DIRECTION = ":"; + + private static final String DELIMITER_SORT_TUPLE = ","; + + /* + * utility constructor private. + */ + private SortUtility() { + } + + /** + * Parses the sort string e.g. given in a REST call based on the definition + * of sorting: http://localhost/entity?s=field1:ASC, field2:DESC The fields + * will be split into the keys of the returned map. The direction of the + * sorting will be mapped into the {@link Direction} enum. + * + * @param enumType + * the class of the enum which the fields in the sort string + * should be related to. + * @param + * the type of the enumeration which must be derived from + * {@link FieldNameProvider} + * @param sortString + * the string representation of the query parameters. Might be + * {@code null} or an empty string. + * @return a list which holds the {@link FieldNameProvider} and the specific + * {@link Direction} for them as a tuple. Never {@code null}. In + * case of no sorting parameters an empty map will be returned. + * @throws SortParameterSyntaxErrorException + * if the sorting query parameter is not well-formed + * @throws SortParameterUnsupportedFieldException + * if a field name cannot be mapped to the enum type + * @throws SortParameterUnsupportedDirectionException + * if the given direction is not "ASC" or "DESC" + */ + public static & FieldNameProvider> List parse(final Class enumType, + final String sortString) throws SortParameterSyntaxErrorException { + final List parsedSortings = Lists.newArrayList(); + // scan the sort tuples e.g. field:direction + if (sortString != null) { + final StringTokenizer tupleTokenizer = new StringTokenizer(sortString, DELIMITER_SORT_TUPLE); + while (tupleTokenizer.hasMoreTokens()) { + final String sortTuple = tupleTokenizer.nextToken().trim(); + final StringTokenizer fieldDirectionTokenizer = new StringTokenizer(sortTuple, + DELIMITER_FIELD_DIRECTION); + if (fieldDirectionTokenizer.countTokens() == 2) { + final String fieldName = fieldDirectionTokenizer.nextToken().trim().toUpperCase(); + final String sortDirectionStr = fieldDirectionTokenizer.nextToken().trim(); + + final T identifier = getAttributeIdentifierByName(enumType, fieldName); + + final Direction sortDirection = getDirection(sortDirectionStr); + parsedSortings.add(new Order(sortDirection, identifier.getFieldName())); + } else { + throw new SortParameterSyntaxErrorException(); + } + } + } + return parsedSortings; + } + + /** + * Returns the attribute identifier for the given name. + * + * @param enumType + * the class of the enum which the fields in the sort string + * should be related to. + * @param name + * the name of the enum + * @param + * the type of the enumeration which must be derived from + * {@link FieldNameProvider} + * @return the corresponding enum + * @throws SortParameterUnsupportedFieldException + * if there is no matching enum for the specified name + */ + private static & FieldNameProvider> T getAttributeIdentifierByName(final Class enumType, + final String name) { + try { + return Enum.valueOf(enumType, name.toUpperCase()); + } catch (final IllegalArgumentException e) { + throw new SortParameterUnsupportedFieldException(e); + } + } + + private static Direction getDirection(final String sortDirectionStr) { + try { + return Direction.fromString(sortDirectionStr); + } catch (final IllegalArgumentException e) { + throw new SortParameterUnsupportedDirectionException(e); + } + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SystemManagementResource.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SystemManagementResource.java new file mode 100755 index 000000000..cd89efcbb --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/SystemManagementResource.java @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.repository.model.TenantConfiguration; +import org.eclipse.hawkbit.rest.resource.model.system.CacheRest; +import org.eclipse.hawkbit.rest.resource.model.system.TenantConfigurationRest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +/** + * {@link SystemManagement} capabilities by REST. + * + * + * + * + */ +@RestController +@RequestMapping(RestConstants.SYSTEM_ADMIN_MAPPING) +@Transactional(readOnly = true) +public class SystemManagementResource { + + private static final Logger LOGGER = LoggerFactory.getLogger(SystemManagementResource.class); + + @Autowired + private SystemManagement systemManagement; + + @Autowired + private CacheManager cacheManager; + + /** + * Deletes the tenant data of a given tenant. USE WITH CARE! + * + * @param tenant + * to delete + * @return HttpStatus.OK + */ + @RequestMapping(method = RequestMethod.DELETE, value = "/tenants/{tenant}") + @Transactional + public ResponseEntity deleteTenant(@PathVariable final String tenant) { + systemManagement.deleteTenant(tenant); + + return new ResponseEntity<>(HttpStatus.OK); + } + + /** + * Returns a list of all caches containing currently. + * + * @return a list of caches for all tenants + */ + @RequestMapping(method = RequestMethod.GET, value = "/caches") + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_SYSTEM_ADMIN) + public ResponseEntity> getCaches() { + final Collection cacheNames = cacheManager.getCacheNames(); + return ResponseEntity.ok(cacheNames.stream().map(cacheName -> cacheManager.getCache(cacheName)) + .map(cache -> cacheRest(cache)).collect(Collectors.toList())); + } + + /** + * Invalidates all caches for all tenants. + * + * @return a list of cache names which has been invalidated + */ + @RequestMapping(method = RequestMethod.DELETE, value = "/caches") + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_SYSTEM_ADMIN) + public ResponseEntity> invalidateCaches() { + final Collection cacheNames = cacheManager.getCacheNames(); + LOGGER.info("Invalidating caches {}", cacheNames); + cacheNames.forEach(cacheName -> cacheManager.getCache(cacheName).clear()); + return ResponseEntity.ok(cacheNames); + } + + /** + * Adds or updates a configuration for a specific tenant to the tenant + * configuration. + * + * @param configuration + * the configuration value to add or update + * @param key + * the key of the configuration to add or update + * @return the response entity with status OK. + */ + @RequestMapping(method = RequestMethod.PUT, value = "/conf/{key}") + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_SYSTEM_ADMIN) + public ResponseEntity addUpdateConfig(@RequestBody final TenantConfigurationRest configuration, + @PathVariable final String key) { + systemManagement.addOrUpdateConfiguration(new TenantConfiguration(key, configuration.getValue())); + return ResponseEntity.ok().build(); + } + + private CacheRest cacheRest(final Cache cache) { + final Object nativeCache = cache.getNativeCache(); + if (nativeCache instanceof com.google.common.cache.Cache) { + return guavaCache(cache, nativeCache); + } else { + return new CacheRest(cache.getName(), Collections.emptyList()); + } + } + + @SuppressWarnings("unchecked") + private CacheRest guavaCache(final Cache cache, final Object nativeCache) { + final com.google.common.cache.Cache guavaCache = (com.google.common.cache.Cache) nativeCache; + final List keys = guavaCache.asMap().keySet().stream().map(key -> key.toString()) + .collect(Collectors.toList()); + return new CacheRest(cache.getName(), keys); + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/TargetMapper.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/TargetMapper.java new file mode 100755 index 000000000..9d7eaaae6 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/TargetMapper.java @@ -0,0 +1,245 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; + +import java.net.URI; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetInfo.PollStatus; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.eclipse.hawkbit.rest.resource.model.PollStatusRest; +import org.eclipse.hawkbit.rest.resource.model.action.ActionRest; +import org.eclipse.hawkbit.rest.resource.model.action.ActionStatusRest; +import org.eclipse.hawkbit.rest.resource.model.action.ActionStatussRest; +import org.eclipse.hawkbit.rest.resource.model.action.ActionsRest; +import org.eclipse.hawkbit.rest.resource.model.target.TargetRequestBody; +import org.eclipse.hawkbit.rest.resource.model.target.TargetRest; +import org.eclipse.hawkbit.rest.resource.model.target.TargetsRest; + +/** + * A mapper which maps repository model to RESTful model representation and + * back. + * + * + * + * + */ +final class TargetMapper { + private TargetMapper() { + // Utility class + } + + private static String getNameOfActionStatusType(final Action.Status type) { + String result = null; + + switch (type) { + case CANCELED: + result = ActionStatusRest.AS_CANCELED; + break; + case ERROR: + result = ActionStatusRest.AS_ERROR; + break; + case FINISHED: + result = ActionStatusRest.AS_FINISHED; + break; + case RETRIEVED: + result = ActionStatusRest.AS_RETRIEVED; + break; + case RUNNING: + result = ActionStatusRest.AS_RUNNING; + break; + case WARNING: + result = ActionStatusRest.AS_WARNING; + break; + default: + return type.name().toLowerCase(); + + } + + return result; + + } + + private static String getType(final Action action) { + if (!action.isCancelingOrCanceled()) { + return ActionRest.ACTION_UPDATE; + } else if (action.isCancelingOrCanceled()) { + return ActionRest.ACTION_CANCEL; + } + + return null; + } + + private static String getUpdateStatusName(final TargetUpdateStatus updatestatus) { + String result = null; + + switch (updatestatus) { + case ERROR: + result = "error"; + break; + case IN_SYNC: + result = "in_sync"; + break; + case PENDING: + result = "pending"; + break; + case REGISTERED: + result = "registered"; + break; + case UNKNOWN: + result = "unknown"; + break; + default: + return updatestatus.name().toLowerCase(); + } + + return result; + } + + static ActionRest toResponse(final String targetId, final Action action, final boolean isActive) { + final ActionRest result = new ActionRest(); + + result.setActionId(action.getId()); + result.setType(getType(action)); + + if (isActive) { + result.setStatus(ActionRest.ACTION_PENDING); + } else { + result.setStatus(ActionRest.ACTION_FINISHED); + } + + RestModelMapper.mapBaseToBase(result, action); + + result.add(linkTo(methodOn(TargetResource.class).getAction(targetId, action.getId())).withRel("self")); + + return result; + } + + static ActionsRest toResponse(final String targetId, final List actions) { + final ActionsRest mappedList = new ActionsRest(); + + for (final Action action : actions) { + final ActionRest response = toResponse(targetId, action, action.isActive()); + mappedList.add(response); + } + return mappedList; + } + + static TargetRest toResponse(final Target target, final boolean includePollStatus) { + if (target == null) { + return null; + } + final TargetRest targetRest = new TargetRest(); + targetRest.setControllerId(target.getControllerId()); + targetRest.setDescription(target.getDescription()); + targetRest.setName(target.getName()); + targetRest.setUpdateStatus(getUpdateStatusName(target.getTargetInfo().getUpdateStatus())); + + final URI address = target.getTargetInfo().getAddress(); + if (address != null) { + targetRest.setIpAddress(address.getHost()); + targetRest.setAddress(address.toString()); + } + + targetRest.setCreatedBy(target.getCreatedBy()); + targetRest.setLastModifiedBy(target.getLastModifiedBy()); + + targetRest.setCreatedAt(target.getCreatedAt()); + targetRest.setLastModifiedAt(target.getLastModifiedAt()); + + targetRest.setSecurityToken(target.getSecurityToken()); + + // last target query is the last controller request date + final Long lastTargetQuery = target.getTargetInfo().getLastTargetQuery(); + final Long installationDate = target.getTargetInfo().getInstallationDate(); + + if (lastTargetQuery != null) { + targetRest.setLastControllerRequestAt(lastTargetQuery); + } + if (installationDate != null) { + targetRest.setInstalledAt(installationDate); + } + + if (includePollStatus) { + final PollStatus pollStatus = target.getTargetInfo().getPollStatus(); + if (pollStatus != null) { + final PollStatusRest pollStatusRest = new PollStatusRest(); + pollStatusRest.setLastRequestAt( + Date.from(pollStatus.getLastPollDate().atZone(ZoneId.systemDefault()).toInstant()).getTime()); + pollStatusRest.setNextExpectedRequestAt( + Date.from(pollStatus.getNextPollDate().atZone(ZoneId.systemDefault()).toInstant()).getTime()); + pollStatusRest.setOverdue(pollStatus.isOverdue()); + targetRest.setPollStatus(pollStatusRest); + } + } + targetRest.add(linkTo(methodOn(TargetResource.class).getTarget(target.getControllerId())).withRel("self")); + + return targetRest; + } + + private static ActionStatusRest toResponse(final Action action, final ActionStatus actionStatus) { + final ActionStatusRest result = new ActionStatusRest(); + + result.setMessages(actionStatus.getMessages()); + result.setReportedAt(action.getCreatedAt()); + result.setStatusId(action.getId()); + result.setType(getNameOfActionStatusType(actionStatus.getStatus())); + + return result; + } + + static List fromRequest(final Iterable targetsRest) { + final List mappedList = new ArrayList<>(); + for (final TargetRequestBody targetRest : targetsRest) { + mappedList.add(fromRequest(targetRest)); + } + return mappedList; + } + + static Target fromRequest(final TargetRequestBody targetRest) { + final Target target = new Target(targetRest.getControllerId()); + target.setDescription(targetRest.getDescription()); + target.setName(targetRest.getName()); + return target; + } + + static ActionStatussRest toActionStatusRestResponse(final Action action, final List actionStatus) { + final ActionStatussRest mappedList = new ActionStatussRest(); + + if (actionStatus != null) { + for (final ActionStatus status : actionStatus) { + final ActionStatusRest response = toResponse(action, status); + mappedList.add(response); + } + } + + return mappedList; + } + + static TargetsRest toResponse(final Iterable targets) { + final TargetsRest mappedList = new TargetsRest(); + if (targets != null) { + for (final Target target : targets) { + // to single response without pollstatus in the list + final TargetRest response = toResponse(target, false); + mappedList.add(response); + } + } + return mappedList; + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/TargetResource.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/TargetResource.java new file mode 100755 index 000000000..0715d5189 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/TargetResource.java @@ -0,0 +1,602 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.persistence.EntityManager; + +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.repository.ActionFields; +import org.eclipse.hawkbit.repository.ActionStatusFields; +import org.eclipse.hawkbit.repository.DeploymentManagement; +import org.eclipse.hawkbit.repository.TargetFields; +import org.eclipse.hawkbit.repository.TargetManagement; +import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.rsql.RSQLUtility; +import org.eclipse.hawkbit.rest.resource.helper.RestResourceConversionHelper; +import org.eclipse.hawkbit.rest.resource.model.ExceptionInfo; +import org.eclipse.hawkbit.rest.resource.model.action.ActionPagedList; +import org.eclipse.hawkbit.rest.resource.model.action.ActionRest; +import org.eclipse.hawkbit.rest.resource.model.action.ActionStatusPagedList; +import org.eclipse.hawkbit.rest.resource.model.distributionset.DistributionSetRest; +import org.eclipse.hawkbit.rest.resource.model.target.DistributionSetAssigmentRest; +import org.eclipse.hawkbit.rest.resource.model.target.TargetAttributes; +import org.eclipse.hawkbit.rest.resource.model.target.TargetPagedList; +import org.eclipse.hawkbit.rest.resource.model.target.TargetRequestBody; +import org.eclipse.hawkbit.rest.resource.model.target.TargetRest; +import org.eclipse.hawkbit.rest.resource.model.target.TargetsRest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * REST Resource handling target CRUD operations. + * + * + * + * + */ +@RestController +@Transactional(readOnly = true) +@RequestMapping(RestConstants.TARGET_V1_REQUEST_MAPPING) +@Api(value = "targets", description = "Provisioning Target Management API") +public class TargetResource { + private static final Logger LOG = LoggerFactory.getLogger(TargetResource.class); + + @Autowired + private TargetManagement targetManagement; + + @Autowired + private DeploymentManagement deploymentManagement; + + @Autowired + private EntityManager entityManager; + + /** + * Handles the GET request of retrieving a single target within SP. + * + * @param targetId + * the ID of the target to retrieve + * @return a single target with status OK. + * @throws EntityNotFoundException + * in case no target with the given {@code targetId} exists. + */ + @RequestMapping(method = RequestMethod.GET, value = "/{targetId}", produces = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = TargetRest.class, value = "Get single Target", notes = "Handles the GET request of retrieving a single target within SP. Required Permission: " + + SpPermission.READ_TARGET) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Target", response = ExceptionInfo.class)) + public ResponseEntity getTarget(@PathVariable final String targetId) { + final Target findTarget = findTargetWithExceptionIfNotFound(targetId); + // to single response include poll status + final TargetRest response = TargetMapper.toResponse(findTarget, true); + // add links + response.add(linkTo(methodOn(TargetResource.class).getAssignedDistributionSet(response.getControllerId())) + .withRel(RestConstants.TARGET_V1_ASSIGNED_DISTRIBUTION_SET)); + response.add(linkTo(methodOn(TargetResource.class).getInstalledDistributionSet(response.getControllerId())) + .withRel(RestConstants.TARGET_V1_INSTALLED_DISTRIBUTION_SET)); + response.add(linkTo(methodOn(TargetResource.class).getAttributes(response.getControllerId())).withRel( + RestConstants.TARGET_V1_ATTRIBUTES)); + response.add(linkTo( + methodOn(TargetResource.class).getActionHistory(response.getControllerId(), 0, + RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT_VALUE, + ActionFields.ID.getFieldName() + ":" + SortDirection.DESC, null)).withRel( + RestConstants.TARGET_V1_ACTIONS)); + + return new ResponseEntity<>(response, HttpStatus.OK); + } + + /** + * Handles the GET request of retrieving all targets within SP. + * + * @param pagingOffsetParam + * the offset of list of targets for pagination, might not be + * present in the rest request then default value will be applied + * @param pagingLimitParam + * the limit of the paged request, might not be present in the + * rest request then default value will be applied + * @param sortParam + * the sorting parameter in the request URL, syntax + * {@code field:direction, field:direction} + * @param rsqlParam + * the search parameter in the request URL, syntax + * {@code q=name==abc} + * @return a list of all targets for a defined or default page request with + * status OK. The response is always paged. In any failure the + * JsonResponseExceptionHandler is handling the response. + */ + @RequestMapping(method = RequestMethod.GET, produces = { "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = TargetPagedList.class, value = "Get paged list of Targets", notes = "Handles the GET request of retrieving all targets within SP. Required Permission: " + + SpPermission.READ_TARGET) + public ResponseEntity getTargets( + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam, + @ApiParam(required = false, value = "FIQL syntax search query" + + "" + + "" + + "" + + "" + + "" + + "" + + "
updatestatus==unknown,updatestatus==errortargets with status 'unknown' or 'error'
installedAt=ge=1424699220targets which installation date later than 1424699220
name=li=%25ccu%25targets which contain 'ccu' in their name ingore case
controllerId==target0815target which has the ID 'target0815'
ipaddress=li=171.%25target which starts with IP address '171.'
") @RequestParam(value = RestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam) { + + final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); + final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); + final Sort sorting = PagingUtility.sanitizeTargetSortParam(sortParam); + + final Pageable pageable = new OffsetBasedPageRequest(sanitizedOffsetParam, sanitizedLimitParam, sorting); + final Slice findTargetsAll; + final Long countTargetsAll; + if (rsqlParam != null) { + final Page findTargetPage = targetManagement.findTargetsAll( + RSQLUtility.parse(rsqlParam, TargetFields.class, entityManager), pageable); + countTargetsAll = findTargetPage.getTotalElements(); + findTargetsAll = findTargetPage; + } else { + findTargetsAll = targetManagement.findTargetsAll(pageable); + countTargetsAll = targetManagement.countTargetsAll(); + } + + final List rest = TargetMapper.toResponse(findTargetsAll.getContent()); + return new ResponseEntity<>(new TargetPagedList(rest, countTargetsAll), HttpStatus.OK); + } + + /** + * Handles the POST request of creating new targets within SP. The request + * body must always be a list of targets. The requests is delegating to the + * {@link TargetManagement#createTarget(Iterable)}. + * + * @param targets + * the targets to be created. + * @return In case all targets could successful created the ResponseEntity + * with status code 201 with a list of successfully created + * entities. In any failure the JsonResponseExceptionHandler is + * handling the response. + */ + @RequestMapping(method = RequestMethod.POST, consumes = { "application/hal+json", MediaType.APPLICATION_JSON_VALUE }, produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = TargetsRest.class, value = "Create list of Targets", notes = "Handles the POST request of creating new targets within SP. The request body must always be a list of targets. Required Permission: " + + SpPermission.CREATE_TARGET) + @ApiResponses(@ApiResponse(code = 409, message = "Conflict", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity createTargets( + @RequestBody @ApiParam(value = "List of targets", required = true) final List targets) { + LOG.debug("creating {} targets", targets.size()); + final Iterable createdTargets = targetManagement.createTargets(TargetMapper.fromRequest(targets)); + LOG.debug("{} targets created, return status {}", targets.size(), HttpStatus.CREATED); + + // we flush to ensure that entity is generated and we can return ID etc. + entityManager.flush(); + + return new ResponseEntity<>(TargetMapper.toResponse(createdTargets), HttpStatus.CREATED); + } + + /** + * Handles the PUT request of updating a target within SP. The ID is within + * the URL path of the request. A given ID in the request body is ignored. + * It's not possible to set fields to {@code null} values. + * + * @param targetId + * the path parameter which contains the ID of the target + * @param targetRest + * the request body which contains the fields which should be + * updated, fields which are not given are ignored for the + * udpate. + * @return the updated target response which contains all fields also fields + * which have not updated + */ + @RequestMapping(method = RequestMethod.PUT, value = "/{targetId}", consumes = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }, produces = { "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = TargetRest.class, value = "Updates a target", notes = "Handles the PUT request of updating a target within SP. Required Permission: " + + SpPermission.UPDATE_TARGET) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Target", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity updateTarget(@PathVariable final String targetId, + @RequestBody @ApiParam(value = "Target to be updated", required = true) final TargetRequestBody targetRest) { + final Target existingTarget = findTargetWithExceptionIfNotFound(targetId); + LOG.debug("updating target {}", existingTarget.getId()); + if (targetRest.getDescription() != null) { + existingTarget.setDescription(targetRest.getDescription()); + } + if (targetRest.getName() != null) { + existingTarget.setName(targetRest.getName()); + } + final Target updateTarget = targetManagement.updateTarget(existingTarget); + LOG.debug("target {} updated, return status {}", updateTarget.getId(), HttpStatus.OK); + + // we flush to ensure that entity is generated and we can return ID etc. + entityManager.flush(); + + return new ResponseEntity<>(TargetMapper.toResponse(updateTarget, false), HttpStatus.OK); + } + + /** + * Handles the DELETE request of deleting a target within SP. + * + * @param targetId + * the ID of the target to be deleted + * @return If the given targetId could exists and could be deleted Http OK. + * In any failure the JsonResponseExceptionHandler is handling the + * response. + */ + @RequestMapping(method = RequestMethod.DELETE, value = "/{targetId}", produces = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(value = "Deletes a target", notes = "Handles the DELETE request of deleting a single target within SP. Required Permission: " + + SpPermission.DELETE_TARGET) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Target", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity deleteTarget(@PathVariable final String targetId) { + final Target target = findTargetWithExceptionIfNotFound(targetId); + targetManagement.deleteTargets(target.getId()); + LOG.debug("{} target deleted, return status {}", targetId, HttpStatus.OK); + return new ResponseEntity<>(HttpStatus.OK); + } + + /** + * Handles the GET request of retrieving the attributes of a specific + * target. + * + * @param targetId + * the ID of the target to retrieve the attributes. + * @return the target attributes as map response with status OK + * @throws EntityNotFoundException + * in case no target with the given {@code targetId} exists. + */ + @RequestMapping(method = RequestMethod.GET, value = "/{targetId}/attributes", produces = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = TargetAttributes.class, value = "Get attributes of Target", notes = "Handles the GET request of retrieving the attributes of a specific target. Reponse is a key/value list. Required Permission: " + + SpPermission.READ_TARGET) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Target", response = ExceptionInfo.class)) + public ResponseEntity getAttributes(@PathVariable final String targetId) { + final Target foundTarget = findTargetWithExceptionIfNotFound(targetId); + final Map controllerAttributes = foundTarget.getTargetInfo().getControllerAttributes(); + if (controllerAttributes.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + final TargetAttributes result = new TargetAttributes(); + result.putAll(controllerAttributes); + + return new ResponseEntity<>(result, HttpStatus.OK); + } + + /** + * Handles the GET request of retrieving the {@link Action}s of a specific + * target. + * + * @param targetId + * to load actions for + * @param pagingOffsetParam + * the offset of list of targets for pagination, might not be + * present in the rest request then default value will be applied + * @param pagingLimitParam + * the limit of the paged request, might not be present in the + * rest request then default value will be applied + * @param sortParam + * the sorting parameter in the request URL, syntax + * {@code field:direction, field:direction} + * @param rsqlParam + * the search parameter in the request URL, syntax + * {@code q=status==pending} + * @return a list of all {@link Action}s for a defined or default page + * request with status OK. The response is always paged. In any + * failure the JsonResponseExceptionHandler is handling the + * response. + */ + @RequestMapping(method = RequestMethod.GET, value = "/{targetId}/actions", produces = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = ActionPagedList.class, value = "List all actions of Target", notes = "Handles the GET request of retrieving the full action history of a specific target. Required Permission: " + + SpPermission.READ_TARGET) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Target", response = ExceptionInfo.class)) + public ResponseEntity getActionHistory( + @PathVariable final String targetId, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam, + @ApiParam(required = false, value = "FIQL syntax search query" + "" + + "" + + "" + + "
status==finishedactions which are already 'finished'
status==pendingactions which are currently in 'pending' state
") @RequestParam(value = RestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam) { + + final Target foundTarget = findTargetWithExceptionIfNotFound(targetId); + + final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); + final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); + final Sort sorting = PagingUtility.sanitizeActionSortParam(sortParam); + final Pageable pageable = new OffsetBasedPageRequest(sanitizedOffsetParam, sanitizedLimitParam, sorting); + + final Slice activeActions; + final Long totalActionCount; + if (rsqlParam != null) { + final Specification parse = RSQLUtility.parse(rsqlParam, ActionFields.class, entityManager); + activeActions = deploymentManagement.findActionsByTarget(parse, foundTarget, pageable); + totalActionCount = deploymentManagement.countActionsByTarget(parse, foundTarget); + } else { + activeActions = deploymentManagement.findActionsByTarget(foundTarget, pageable); + totalActionCount = deploymentManagement.countActionsByTarget(foundTarget); + } + + return new ResponseEntity(new ActionPagedList(TargetMapper.toResponse(targetId, + activeActions.getContent()), totalActionCount), HttpStatus.OK); + } + + /** + * Handles the GET request of retrieving a specific {@link Action}s of a + * specific {@link Target}. + * + * @param targetId + * to load the action for + * @param actionId + * to load + * @return the action + */ + @RequestMapping(method = RequestMethod.GET, value = "/{targetId}/actions/{actionId}", produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = ActionRest.class, value = "Get assigned action of Target", notes = "Handles the GET request of retrieving a specific action on a specific target. Required Permission: " + + SpPermission.READ_TARGET) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Target or Action", response = ExceptionInfo.class)) + public ResponseEntity getAction(@PathVariable final String targetId, @PathVariable final Long actionId) { + final Target target = findTargetWithExceptionIfNotFound(targetId); + + final Action action = findActionWithExceptionIfNotFound(actionId); + if (!action.getTarget().getId().equals(target.getId())) { + LOG.warn("given action ({}) is not assigned to given target ({}).", action.getId(), target.getId()); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + final ActionRest result = TargetMapper.toResponse(targetId, action, action.isActive()); + + if (!action.isCancelingOrCanceled()) { + result.add(linkTo( + methodOn(DistributionSetResource.class).getDistributionSet(action.getDistributionSet().getId())) + .withRel("distributionset")); + } else if (action.isCancelingOrCanceled()) { + result.add(linkTo(methodOn(TargetResource.class).getAction(targetId, action.getId())).withRel( + RestConstants.TARGET_V1_CANCELED_ACTION)); + } + + result.add(linkTo( + methodOn(TargetResource.class).getActionStatusList(targetId, action.getId(), 0, + RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT_VALUE, + ActionStatusFields.ID.getFieldName() + ":" + SortDirection.DESC)).withRel( + RestConstants.TARGET_V1_ACTION_STATUS)); + + return new ResponseEntity(result, HttpStatus.OK); + } + + /** + * Handles the DELETE request of canceling an specific {@link Action}s of a + * specific {@link Target}. + * + * @param targetId + * the ID of the target in the URL path parameter + * @param actionId + * the ID of the action in the URL path parameter + * @return status no content in case cancellation was successful + * @throws CancelActionNotAllowedException + * if the given action is not active and cannot be canceled. + * @throws EntityNotFoundException + * if the target or the action is not found + */ + @RequestMapping(method = RequestMethod.DELETE, value = "/{targetId}/actions/{actionId}") + @ApiOperation(value = "Cancels an active action", notes = "Cancels an active action, only active actions can be deleted. Required Permission: " + + SpPermission.UPDATE_TARGET) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Target or Action", response = ExceptionInfo.class)) + public ResponseEntity cancelAction(@PathVariable final String targetId, @PathVariable final Long actionId) { + final Target target = findTargetWithExceptionIfNotFound(targetId); + final Action action = findActionWithExceptionIfNotFound(actionId); + + // cancel action will throw an exception if action cannot be canceled + // which is mapped by + // response exception handler. + deploymentManagement.cancelAction(action, target); + + return new ResponseEntity(HttpStatus.NO_CONTENT); + + } + + /** + * Handles the GET request of retrieving the {@link ActionStatus}s of a + * specific target and action. + * + * @param targetId + * of the the action + * @param actionId + * of the status we are intend to load + * @param pagingOffsetParam + * the offset of list of targets for pagination, might not be + * present in the rest request then default value will be applied + * @param pagingLimitParam + * the limit of the paged request, might not be present in the + * rest request then default value will be applied + * @param sortParam + * the sorting parameter in the request URL, syntax + * {@code field:direction, field:direction} + * @return a list of all {@link ActionStatus}s for a defined or default page + * request with status OK. The response is always paged. In any + * failure the JsonResponseExceptionHandler is handling the + * response. + */ + @RequestMapping(method = RequestMethod.GET, value = "/{targetId}/actions/{actionId}/status", produces = { + "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = ActionStatusPagedList.class, value = "Get assigned action of Target", notes = "Handles the GET request of retrieving a specific action on a specific target. Required Permission: " + + SpPermission.READ_TARGET) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Target or Action", response = ExceptionInfo.class)) + public ResponseEntity getActionStatusList( + @PathVariable final String targetId, + @PathVariable final Long actionId, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = RestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, + @RequestParam(value = RestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam) { + + final Target target = findTargetWithExceptionIfNotFound(targetId); + + final Action action = findActionWithExceptionIfNotFound(actionId); + if (!action.getTarget().getId().equals(target.getId())) { + LOG.warn("given action ({}) is not assigned to given target ({}).", action.getId(), target.getId()); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); + final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); + final Sort sorting = PagingUtility.sanitizeActionStatusSortParam(sortParam); + + final Page statusList = deploymentManagement.findActionStatusMessagesByActionInDescOrder( + new OffsetBasedPageRequest(sanitizedOffsetParam, sanitizedLimitParam, sorting), action, true); + + return new ResponseEntity<>(new ActionStatusPagedList(TargetMapper.toActionStatusRestResponse(action, + statusList.getContent()), statusList.getTotalElements()), HttpStatus.OK); + + } + + /** + * Handles the GET request of retrieving the assigned distribution set of an + * specific target. + * + * @param targetId + * the ID of the target to retrieve the assigned distribution + * @return the assigned distribution set with status OK, if none is assigned + * than {@code null} content (e.g. "{}") + * @throws EntityNotFoundException + * in case no target with the given {@code targetId} exists. + */ + @RequestMapping(method = RequestMethod.GET, value = "/{targetId}/assignedDS", produces = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = DistributionSetRest.class, value = "Get assigned Distribution Set of Target", notes = "Handles the GET request of retrieving the assigned distribution set of an specific target. Required Permission: " + + SpPermission.READ_TARGET) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Target", response = ExceptionInfo.class)) + public ResponseEntity getAssignedDistributionSet(@PathVariable final String targetId) { + final Target findTarget = findTargetWithExceptionIfNotFound(targetId); + final DistributionSetRest distributionSetRest = DistributionSetMapper.toResponse(findTarget + .getAssignedDistributionSet()); + final HttpStatus retStatus; + if (distributionSetRest == null) { + retStatus = HttpStatus.NO_CONTENT; + } else { + retStatus = HttpStatus.OK; + } + return new ResponseEntity<>(distributionSetRest, retStatus); + } + + /** + * Changes the assigned distribution set of a target. + * + * @param targetId + * of the target to change + * @param dsId + * of the distributionset that is to be assigned + * @return {@link HttpStatus#OK} + * + * @throws EntityNotFoundException + * in case no target with the given {@code targetId} exists. + * + */ + @RequestMapping(method = RequestMethod.POST, value = "/{targetId}/assignedDS", consumes = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }, produces = { "application/hal+json", MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = Void.class, value = "Assigned Distribution Set to Target", notes = "Handles the POST request for assigning a distribution set to a specific target. Required Permission: " + + SpPermission.READ_REPOSITORY + " and " + SpPermission.UPDATE_TARGET) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Target", response = ExceptionInfo.class)) + @Transactional + public ResponseEntity postAssignedDistributionSet( + @PathVariable final String targetId, + @RequestBody @ApiParam(value = "Distribution Set ID", required = true) final DistributionSetAssigmentRest dsId) { + + findTargetWithExceptionIfNotFound(targetId); + final ActionType type = (dsId.getType() != null) ? RestResourceConversionHelper.convertActionType(dsId + .getType()) : ActionType.FORCED; + final Iterator changed = deploymentManagement + .assignDistributionSet(dsId.getId(), type, dsId.getForcetime(), targetId).getAssignedTargets() + .iterator(); + if (changed.hasNext()) { + return new ResponseEntity<>(HttpStatus.OK); + } + + LOG.error("Target update (ds {} assigment to target {}) failed! Returnd target list is empty.", dsId.getId(), + targetId); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + + } + + /** + * Handles the GET request of retrieving the installed distribution set of + * an specific target. + * + * @param targetId + * the ID of the target to retrieve + * @return the assigned installed set with status OK, if none is installed + * than {@code null} content (e.g. "{}") + * @throws EntityNotFoundException + * in case no target with the given {@code targetId} exists. + */ + @RequestMapping(method = RequestMethod.GET, value = "/{targetId}/installedDS", produces = { "application/hal+json", + MediaType.APPLICATION_JSON_VALUE }) + @ApiOperation(response = DistributionSetRest.class, value = "Get installed Distribution Set of Target", notes = "Handles the GET request of retrieving the installed distribution set of an specific target. Required Permission: " + + SpPermission.READ_TARGET) + @ApiResponses(@ApiResponse(code = 404, message = "Not Found Target", response = ExceptionInfo.class)) + public ResponseEntity getInstalledDistributionSet(@PathVariable final String targetId) { + final Target findTarget = findTargetWithExceptionIfNotFound(targetId); + final DistributionSetRest distributionSetRest = DistributionSetMapper.toResponse(findTarget.getTargetInfo() + .getInstalledDistributionSet()); + final HttpStatus retStatus; + if (distributionSetRest == null) { + retStatus = HttpStatus.NO_CONTENT; + } else { + retStatus = HttpStatus.OK; + } + return new ResponseEntity<>(distributionSetRest, retStatus); + } + + private Target findTargetWithExceptionIfNotFound(final String targetId) { + final Target findTarget = targetManagement.findTargetByControllerID(targetId); + if (findTarget == null) { + throw new EntityNotFoundException("Target with Id {" + targetId + "} does not exist"); + } + return findTarget; + } + + private Action findActionWithExceptionIfNotFound(final Long actionId) { + final Action findAction = deploymentManagement.findAction(actionId); + if (findAction == null) { + throw new EntityNotFoundException("Action with Id {" + actionId + "} does not exist"); + } + return findAction; + } +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/helper/ByteRange.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/helper/ByteRange.java new file mode 100755 index 000000000..df72e2c53 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/helper/ByteRange.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.helper; + +/** + * Byte range for resume download operations. + * + * + * + * + * + */ +public class ByteRange { + public static final String MULTIPART_BOUNDARY = "THIS_STRING_SEPARATES_MULTIPART"; + + private final long start; + private final long end; + private final long length; + private final long total; + + /** + * Construct a byte range. + * + * @param start + * Start of the byte range. + * @param end + * End of the byte range. + * @param total + * Total length of the byte source. + */ + public ByteRange(final long start, final long end, final long total) { + this.start = start; + this.end = end; + length = end - start + 1; + this.total = total; + } + + /** + * @return the start + */ + public long getStart() { + return start; + } + + /** + * @return the end + */ + public long getEnd() { + return end; + } + + /** + * @return the length + */ + public long getLength() { + return length; + } + + /** + * @return the total + */ + public long getTotal() { + return total; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { // NOSONAR - as this is generated + final int prime = 31; + int result = 1; + result = prime * result + (int) (end ^ end >>> 32); + result = prime * result + (int) (length ^ length >>> 32); + result = prime * result + (int) (start ^ start >>> 32); + result = prime * result + (int) (total ^ total >>> 32); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { // NOSONAR - as this is generated + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ByteRange other = (ByteRange) obj; + if (end != other.end) { + return false; + } + if (length != other.length) { + return false; + } + if (start != other.start) { + return false; + } + if (total != other.total) { + return false; + } + return true; + } + +} diff --git a/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/helper/RestResourceConversionHelper.java b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/helper/RestResourceConversionHelper.java new file mode 100755 index 000000000..e7b588222 --- /dev/null +++ b/hawkbit-rest-resource/src/main/java/org/eclipse/hawkbit/rest/resource/helper/RestResourceConversionHelper.java @@ -0,0 +1,374 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.helper; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; +import org.eclipse.hawkbit.cache.CacheWriteNotify; +import org.eclipse.hawkbit.controller.FileSteamingFailedException; +import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.rest.resource.model.distributionset.ActionTypeRest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; + +import com.google.common.math.DoubleMath; +import com.google.common.net.HttpHeaders; + +/** + * Utility class for the Rest Source API. + * + * + * + * + */ +public final class RestResourceConversionHelper { + private static final Logger LOG = LoggerFactory.getLogger(RestResourceConversionHelper.class); + + private static final int BUFFER_SIZE = 4096; + + // utility class, private constructor. + private RestResourceConversionHelper() { + + } + + /** + * Write response without target relation. + * + * @param artifact + * the artifact + * @param servletResponse + * to be sent back to the requesting client + * @param request + * from the client + * @param file + * to be write to the client response + * + * @return http code + */ + public static ResponseEntity writeFileResponse(final LocalArtifact artifact, + final HttpServletResponse servletResponse, final HttpServletRequest request, final DbArtifact file) { + return writeFileResponse(artifact, servletResponse, request, file, null, null); + } + + /** + *

+ * Write response with target relation and publishes events concerning the + * download progress based on given {@link UpdateActionStatus}. + *

+ * + *

+ * The request supports RFC7233 range requests. + *

+ * + * @param artifact + * the artifact + * @param response + * to be sent back to the requesting client + * @param request + * from the client + * @param file + * to be write to the client response + * @param cacheWriteNotify + * to write progress updates to + * @param statusId + * of the UpdateActionStatus + * + * @throws IOException + * in case of exceptions + * + * @return http code + * + * @see https://tools.ietf.org/html/rfc7233 + */ + public static ResponseEntity writeFileResponse(final LocalArtifact artifact, + final HttpServletResponse response, final HttpServletRequest request, final DbArtifact file, + final CacheWriteNotify cacheWriteNotify, final Long statusId) { + + ResponseEntity result = null; + + final String etag = artifact.getSha1Hash(); + final Long lastModified = artifact.getLastModifiedAt() != null ? artifact.getLastModifiedAt() + : artifact.getCreatedAt(); + final long length = file.getSize(); + + response.reset(); + response.setBufferSize(BUFFER_SIZE); + response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + artifact.getFilename()); + response.setHeader(HttpHeaders.ETAG, etag); + response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes"); + response.setDateHeader(HttpHeaders.LAST_MODIFIED, lastModified); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + + final ByteRange full = new ByteRange(0, length - 1, length); + final List ranges = new ArrayList<>(); + + // Validate and process Range and If-Range headers. + final String range = request.getHeader("Range"); + if (range != null) { + LOG.debug("range header for filename ({}) is: {}", artifact.getFilename(), range); + + // Range header matches"bytes=n-n,n-n,n-n..." + if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) { + response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + length); + LOG.debug("range header for filename ({}) is not satisfiable: ", artifact.getFilename()); + return new ResponseEntity<>(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE); + } + + // RFC: if the representation is unchanged, send me the part(s) that + // I am requesting in + // Range; otherwise, send me the entire representation. + checkForShortcut(request, etag, lastModified, full, ranges); + + // it seems there are valid ranges + result = extractRange(response, length, ranges, range); + // return if range extraction turned out to be invalid + if (result != null) { + return result; + } + } + + // full request - no range + if (ranges.isEmpty() || ranges.get(0).equals(full)) { + LOG.debug("filename ({}) results into a full request: ", artifact.getFilename()); + fullfileRequest(artifact, response, file, cacheWriteNotify, statusId, full); + result = new ResponseEntity<>(HttpStatus.OK); + } + // standard range request + else if (ranges.size() == 1) { + LOG.debug("filename ({}) results into a standard range request: ", artifact.getFilename()); + standardRangeRequest(artifact, response, file, cacheWriteNotify, statusId, ranges); + result = new ResponseEntity<>(HttpStatus.PARTIAL_CONTENT); + } + // multipart range request + else { + LOG.debug("filename ({}) results into a multipart range request: ", artifact.getFilename()); + multipartRangeRequest(artifact, response, file, cacheWriteNotify, statusId, ranges); + result = new ResponseEntity<>(HttpStatus.PARTIAL_CONTENT); + } + + return result; + + } + + private static void fullfileRequest(final LocalArtifact artifact, final HttpServletResponse response, + final DbArtifact file, final CacheWriteNotify cacheWriteNotify, final Long statusId, final ByteRange full) { + final ByteRange r = full; + response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal()); + response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(r.getLength())); + + try { + copyStreams(file.getFileInputStream(), response.getOutputStream(), cacheWriteNotify, statusId, r.getStart(), + r.getLength()); + } catch (final IOException e) { + LOG.error("fullfileRequest of file ({}) failed!", artifact.getFilename(), e); + throw new FileSteamingFailedException(artifact.getFilename()); + } + } + + private static ResponseEntity extractRange(final HttpServletResponse response, final long length, + final List ranges, final String range) { + ResponseEntity result = null; + if (ranges.isEmpty()) { + for (final String part : range.substring(6).split(",")) { + long start = sublong(part, 0, part.indexOf('-')); + long end = sublong(part, part.indexOf('-') + 1, part.length()); + + if (start == -1) { + start = length - end; + end = length - 1; + } else if (end == -1 || end > length - 1) { + end = length - 1; + } + + // Check if Range is syntactically valid. If not, then return + // 416. + if (start > end) { + response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + length); + result = new ResponseEntity<>(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE); + return result; + } + + // Add range. + ranges.add(new ByteRange(start, end, length)); + } + } + + return null; + } + + private static long sublong(final String value, final int beginIndex, final int endIndex) { + final String substring = value.substring(beginIndex, endIndex); + return substring.length() > 0 ? Long.parseLong(substring) : -1; + } + + private static void checkForShortcut(final HttpServletRequest request, final String etag, final long lastModified, + final ByteRange full, final List ranges) { + final String ifRange = request.getHeader(HttpHeaders.IF_RANGE); + if (ifRange != null && !ifRange.equals(etag)) { + try { + final long ifRangeTime = request.getDateHeader(HttpHeaders.IF_RANGE); + if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) { + ranges.add(full); + } + } catch (final IllegalArgumentException ignore) { + LOG.info("Invalid if-range header field", ignore); + ranges.add(full); + } + } + } + + private static void multipartRangeRequest(final LocalArtifact artifact, final HttpServletResponse response, + final DbArtifact file, final CacheWriteNotify cacheWriteNotify, final Long statusId, + final List ranges) { + response.setContentType("multipart/byteranges; boundary=" + ByteRange.MULTIPART_BOUNDARY); + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + + try { + for (final ByteRange r : ranges) { + // Add multipart boundary and header fields for every range. + response.getOutputStream().println(); + response.getOutputStream().println("--" + ByteRange.MULTIPART_BOUNDARY); + response.getOutputStream() + .println("Content-Range: bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal()); + + // Copy single part range of multi part range. + copyStreams(file.getFileInputStream(), response.getOutputStream(), cacheWriteNotify, statusId, + r.getStart(), r.getLength()); + } + + // End with final multipart boundary. + response.getOutputStream().println(); + response.getOutputStream().print("--" + ByteRange.MULTIPART_BOUNDARY + "--"); + } catch (final IOException e) { + LOG.error("multipartRangeRequest of file ({}) failed!", artifact.getFilename(), e); + throw new FileSteamingFailedException(artifact.getFilename()); + } + } + + private static void standardRangeRequest(final LocalArtifact artifact, final HttpServletResponse response, + final DbArtifact file, final CacheWriteNotify cacheWriteNotify, final Long statusId, + final List ranges) { + final ByteRange r = ranges.get(0); + response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal()); + response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(r.getLength())); + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + + try { + copyStreams(file.getFileInputStream(), response.getOutputStream(), cacheWriteNotify, statusId, r.getStart(), + r.getLength()); + } catch (final IOException e) { + LOG.error("standardRangeRequest of file ({}) failed!", artifact.getFilename(), e); + throw new FileSteamingFailedException(artifact.getFilename()); + } + } + + private static long copyStreams(final InputStream from, final OutputStream to, + final CacheWriteNotify cacheWriteNotify, final Long statusId, final long start, final long length) + throws IOException { + checkNotNull(from); + checkNotNull(to); + final byte[] buf = new byte[BUFFER_SIZE]; + long total = 0; + int progressPercent = 1; + + // skipp until start is reached + long skipped = 0; + do { + skipped += from.skip(start); + } while (skipped < start); + + long toRead = length; + boolean toContinue = true; + + while (toContinue) { + final int r = from.read(buf); + if (r == -1) { + break; + } + + toRead -= r; + if (toRead > 0) { + to.write(buf, 0, r); + total += r; + } else { + to.write(buf, 0, (int) toRead + r); + total += toRead + r; + toContinue = false; + } + + if (cacheWriteNotify != null) { + final int newPercent = DoubleMath.roundToInt(total * 100.0 / length, RoundingMode.DOWN); + + // every 10 percent an event + if (newPercent == 100 || newPercent > progressPercent + 10) { + progressPercent = newPercent; + cacheWriteNotify.downloadProgressPercent(statusId, progressPercent); + } + } + } + return total; + } + + /** + * Checks given CSV string for defined match value or * wildcard. + * + * @param matchHeader + * to search through + * @param toMatch + * to search for + * + * @return true if string matches. + */ + public static boolean matchesHttpHeader(final String matchHeader, final String toMatch) { + final String[] matchValues = matchHeader.split("\\s*,\\s*"); + Arrays.sort(matchValues); + return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1; + } + + /** + * Convert a action rest type to a action repository type. + * + * @param actionTypeRest + * the rest type + * @return or the action repository type + */ + public static ActionType convertActionType(final ActionTypeRest actionTypeRest) { + if (actionTypeRest == null) { + return null; + } + + switch (actionTypeRest) { + case SOFT: + return ActionType.SOFT; + case FORCED: + return ActionType.FORCED; + case TIMEFORCED: + return ActionType.TIMEFORCED; + default: + throw new IllegalStateException("Action Type is not supported"); + } + + } +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/MockMvcResultPrinter.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/MockMvcResultPrinter.java new file mode 100755 index 000000000..b01bb4e9f --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/MockMvcResultPrinter.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultHandler; +import org.springframework.test.web.servlet.result.PrintingResultHandler; +import org.springframework.util.CollectionUtils; + +public abstract class MockMvcResultPrinter { + private static final Logger LOG = LoggerFactory.getLogger(MockMvcResultPrinter.class); + + private MockMvcResultPrinter() { + } + + /** + * Print {@link MvcResult} details to the "standard" output stream. + */ + public static ResultHandler print() { + return new ConsolePrintingResultHandler(); + } + + /** + * An {@link PrintingResultHandler} that writes to the "standard" output + * stream + */ + private static class ConsolePrintingResultHandler extends PrintingResultHandler { + + public ConsolePrintingResultHandler() { + super(new ResultValuePrinter() { + + @Override + public void printHeading(final String heading) { + LOG.debug(String.format("%20s:", heading)); + } + + @Override + public void printValue(final String label, Object value) { + if (value != null && value.getClass().isArray()) { + value = CollectionUtils.arrayToList(value); + } + LOG.debug(String.format("%20s = %s", label, value)); + } + }); + } + } +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/controller/ArtifactDownloadTest.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/controller/ArtifactDownloadTest.java new file mode 100755 index 000000000..a87ce748a --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/controller/ArtifactDownloadTest.java @@ -0,0 +1,576 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.RandomUtils; +import org.eclipse.hawkbit.AbstractIntegrationTestWithMongoDB; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.WithUser; +import org.eclipse.hawkbit.eventbus.event.DownloadProgressEvent; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.Artifact; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.repository.model.Target; +import org.junit.Test; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MvcResult; + +import com.google.common.base.Charsets; +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; +import com.google.common.net.HttpHeaders; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * Test artifact downloads from the controller. + * + * + * + * + */ + +@ActiveProfiles({ "im", "test" }) +@Features("Component Tests - Controller RESTful API") +@Stories("Artifact Download Resource") +public class ArtifactDownloadTest extends AbstractIntegrationTestWithMongoDB { + + private static final String AUTH_ANOYM = "ROLE_CONTROLLER_ANONYMOUS"; + + public ArtifactDownloadTest() { + LOG = LoggerFactory.getLogger(ArtifactDownloadTest.class); + } + + private int downLoadProgress = 0; + + @Autowired + private EventBus eventBus; + + @Test + @Description("Tests non allowed requests on the artifact ressource, e.g. invalid URI, wrong if-match, wrong command.") + public void invalidRequestsOnArtifactResource() throws Exception { + // create target + Target target = new Target("4712"); + target = targetManagement.createTarget(target); + final List targets = new ArrayList<>(); + targets.add(target); + + // create ds + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + deploymentManagement.assignDistributionSet(ds, targets); + + // create artifact + final byte random[] = RandomUtils.nextBytes(5 * 1024); + final LocalArtifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + ds.findFirstModuleByType(osType).getId(), "file1", false); + + // no artifact available + mvc.perform(get("/controller/v1/{targetid}/softwaremodules/{softwareModuleId}/artifacts/123455", + target.getControllerId(), ds.findFirstModuleByType(osType).getId())).andExpect(status().isNotFound()); + mvc.perform(get("/controller/v1/{targetid}/softwaremodules/{softwareModuleId}/artifacts/123455.MD5SUM", + target.getControllerId(), ds.findFirstModuleByType(osType).getId())).andExpect(status().isNotFound()); + + // SM does not exist + mvc.perform(get("/controller/v1/{targetid}/softwaremodules/1234567890/artifacts/{filename}", + target.getControllerId(), artifact.getFilename())).andExpect(status().isNotFound()); + mvc.perform(get("/controller/v1/{targetid}/softwaremodules/1234567890/artifacts/{filename}.MD5SUM", + target.getControllerId(), artifact.getFilename())).andExpect(status().isNotFound()); + + // test now consistent data to test allowed methods + mvc.perform(get("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}", + tenantAware.getCurrentTenant(), target.getControllerId(), ds.findFirstModuleByType(osType).getId(), + artifact.getFilename()).header(HttpHeaders.IF_MATCH, artifact.getSha1Hash())) + .andExpect(status().isOk()); + mvc.perform(get("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM", + tenantAware.getCurrentTenant(), target.getControllerId(), ds.findFirstModuleByType(osType).getId(), + artifact.getFilename())).andExpect(status().isOk()); + + // test failed If-match + mvc.perform(get("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}", + tenantAware.getCurrentTenant(), target.getControllerId(), ds.findFirstModuleByType(osType).getId(), + artifact.getFilename()).header("If-Match", "fsjkhgjfdhg")).andExpect(status().isPreconditionFailed()); + + // test invalid range + mvc.perform(get("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}", + tenantAware.getCurrentTenant(), target.getControllerId(), ds.findFirstModuleByType(osType).getId(), + artifact.getFilename()).header("Range", "bytes=1-10,hdsfjksdh")) + .andExpect(header().string("Content-Range", "bytes */" + 5 * 1024)) + .andExpect(status().isRequestedRangeNotSatisfiable()); + + mvc.perform(get("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}", + tenantAware.getCurrentTenant(), target.getControllerId(), ds.findFirstModuleByType(osType).getId(), + artifact.getFilename()).header("Range", "bytes=100-10")) + .andExpect(header().string("Content-Range", "bytes */" + 5 * 1024)) + .andExpect(status().isRequestedRangeNotSatisfiable()); + + // not allowed methods + mvc.perform(put("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}", + tenantAware.getCurrentTenant(), target.getControllerId(), ds.findFirstModuleByType(osType).getId(), + artifact.getFilename())).andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}", + tenantAware.getCurrentTenant(), target.getControllerId(), ds.findFirstModuleByType(osType).getId(), + artifact.getFilename())).andExpect(status().isMethodNotAllowed()); + + mvc.perform(post("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}", + tenantAware.getCurrentTenant(), target.getControllerId(), ds.findFirstModuleByType(osType).getId(), + artifact.getFilename())).andExpect(status().isMethodNotAllowed()); + + mvc.perform(put("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM", + tenantAware.getCurrentTenant(), target.getControllerId(), ds.findFirstModuleByType(osType).getId(), + artifact.getFilename())).andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM", + tenantAware.getCurrentTenant(), target.getControllerId(), ds.findFirstModuleByType(osType).getId(), + artifact.getFilename())).andExpect(status().isMethodNotAllowed()); + + mvc.perform(post("/{tenant}/controller/v1/{targetid}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM", + tenantAware.getCurrentTenant(), target.getControllerId(), ds.findFirstModuleByType(osType).getId(), + artifact.getFilename())).andExpect(status().isMethodNotAllowed()); + } + + @Test + @WithUser(principal = "4712", authorities = "ROLE_CONTROLLER", allSpPermissions = true) + @Description("Tests non allowed requests on the artifact ressource, e.g. invalid URI, wrong if-match, wrong command.") + public void invalidRequestsOnArtifactResourceByName() throws Exception { + // create target + Target target = new Target("4712"); + target = targetManagement.createTarget(target); + final List targets = new ArrayList<>(); + targets.add(target); + + // create ds + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + deploymentManagement.assignDistributionSet(ds, targets); + + // create artifact + final byte random[] = RandomUtils.nextBytes(5 * 1024); + + // Binary + // no artifact available + mvc.perform(get("/controller/artifacts/v1/filename/{filename}", "file1")).andExpect(status().isNotFound()); + + // no artifact available + mvc.perform(get("/controller/artifacts/v1/filename/{filename}.MD5SUM", "file1")) + .andExpect(status().isNotFound()); + + // test now consistent data to test allowed methods + final Artifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + ds.findFirstModuleByType(osType).getId(), "file1", false); + + mvc.perform( + get("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), "file1") + .header(HttpHeaders.IF_MATCH, artifact.getSha1Hash())) + .andExpect(status().isOk()); + + mvc.perform(get("/{tenant}/controller/artifacts/v1/filename/{filename}.MD5SUM", tenantAware.getCurrentTenant(), + "file1")).andExpect(status().isOk()); + + // test failed If-match + mvc.perform( + get("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), "file1") + .header("If-Match", "fsjkhgjfdhg")) + .andExpect(status().isPreconditionFailed()); + + // test invalid range + mvc.perform( + get("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), "file1") + .header("Range", "bytes=1-10,hdsfjksdh")) + .andExpect(header().string("Content-Range", "bytes */" + 5 * 1024)) + .andExpect(status().isRequestedRangeNotSatisfiable()); + + mvc.perform( + get("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), "file1") + .header("Range", "bytes=100-10")) + .andExpect(header().string("Content-Range", "bytes */" + 5 * 1024)) + .andExpect(status().isRequestedRangeNotSatisfiable()); + + // not allowed methods + mvc.perform( + put("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), "file1")) + .andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), + "file1")).andExpect(status().isMethodNotAllowed()); + + mvc.perform( + post("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), "file1")) + .andExpect(status().isMethodNotAllowed()); + + // not allowed methods + mvc.perform(put("/{tenant}/controller/artifacts/v1/filename/{filename}.MD5SUM", tenantAware.getCurrentTenant(), + "file1")).andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete("/{tenant}/controller/artifacts/v1/filename/{filename}.MD5SUM", + tenantAware.getCurrentTenant(), "file1")).andExpect(status().isMethodNotAllowed()); + + mvc.perform(post("/{tenant}/controller/artifacts/v1/filename/{filename}.MD5SUM", tenantAware.getCurrentTenant(), + "file1")).andExpect(status().isMethodNotAllowed()); + + } + + @Test + @WithUser(principal = "4712", authorities = "ROLE_CONTROLLER", allSpPermissions = true) + @Description("Tests valid downloads through the artifact resource by identifying the artifact not by ID but file name.") + public void downloadArtifactThroughFileName() throws Exception { + downLoadProgress = 1; + eventBus.register(this); + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + assertThat(artifactRepository.findAll()).hasSize(0); + + // create target + Target target = new Target("4712"); + target = targetManagement.createTarget(target); + final List targets = new ArrayList(); + targets.add(target); + + // create ds + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + // create artifact + final byte random[] = RandomUtils.nextBytes(5 * 1024 * 1024); + final LocalArtifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + ds.findFirstModuleByType(osType).getId(), "file1", false); + + // download fails as artifact is not yet assigned + mvc.perform(get("/controller/v1/{targetid}/softwaremodules/{softwareModuleId}/artifacts/{filename}", + target.getControllerId(), ds.findFirstModuleByType(osType).getId(), artifact.getFilename())) + .andExpect(status().isNotFound()); + + // now assign and download successful + deploymentManagement.assignDistributionSet(ds, targets); + final MvcResult result = mvc + .perform( + get("/{tenant}/controller/v1/{targetid}/softwaremodules/{softwareModuleId}/artifacts/{filename}", + tenantAware.getCurrentTenant(), target.getControllerId(), + ds.findFirstModuleByType(osType).getId(), artifact.getFilename())) + .andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM)) + .andExpect(header().string("Accept-Ranges", "bytes")) + .andExpect(header().longValue("Last-Modified", artifact.getCreatedAt())) + .andExpect(header().string("Content-Disposition", "attachment;filename=" + artifact.getFilename())) + .andReturn(); + + assertTrue(Arrays.equals(result.getResponse().getContentAsByteArray(), random)); + + // download complete + assertThat(downLoadProgress).isEqualTo(10); + } + + @Test + @Description("Tests valid MD5SUm file downloads through the artifact resource by identifying the artifact by ID.") + public void downloadMd5sumThroughControllerApi() throws Exception { + // create target + Target target = new Target("4712"); + target = targetManagement.createTarget(target); + + // create ds + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + // create artifact + final byte random[] = RandomUtils.nextBytes(5 * 1024); + final LocalArtifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + ds.findFirstModuleByType(osType).getId(), "file1", false); + + // download + final MvcResult result = mvc + .perform( + get("/{tenant}/controller/v1/{targetid}/softwaremodules/{softwareModuleId}/artifacts/{filename}.MD5SUM", + tenantAware.getCurrentTenant(), target.getControllerId(), + ds.findFirstModuleByType(osType).getId(), artifact.getFilename())) + .andExpect(status().isOk()).andExpect(header().string("Content-Disposition", + "attachment;filename=" + artifact.getFilename() + ".MD5SUM")) + .andReturn(); + + assertThat(result.getResponse().getContentAsByteArray()).isEqualTo( + new String(artifact.getMd5Hash() + " " + artifact.getFilename()).getBytes(Charsets.US_ASCII)); + } + + @Test + @WithUser(authorities = "ROLE_CONTROLLER_ANONYMOUS", allSpPermissions = true) + @Description("Ensures that even an authenticated controller is not permitted to download if " + + "anonymous as authorization is notpossible, e.g. chekc if the controller has the artifact assigned.") + public void downloadArtifactByNameFailsIfNotAuthenticated() throws Exception { + downLoadProgress = 1; + eventBus.register(this); + + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + assertThat(artifactRepository.findAll()).hasSize(0); + + // create target + Target target = new Target("4712"); + target = targetManagement.createTarget(target); + final List targets = new ArrayList(); + targets.add(target); + + // create ds + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + // create artifact + final byte random[] = RandomUtils.nextBytes(5 * 1024); + final Artifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + ds.findFirstModuleByType(osType).getId(), "file1.tar.bz2", false); + + // download fails as artifact is not yet assigned to target + deploymentManagement.assignDistributionSet(ds, targets); + mvc.perform(get("/controller/artifacts/v1/filename/{filename}", "file1.tar.bz2")) + .andExpect(status().isNotFound()); + } + + @Test + @WithUser(principal = "4712", authorities = "ROLE_CONTROLLER", allSpPermissions = true) + @Description("Ensures that an authenticated and named controller is permitted to download.") + public void downloadArtifactByNameByNamedController() throws Exception { + downLoadProgress = 1; + eventBus.register(this); + + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + assertThat(artifactRepository.findAll()).hasSize(0); + + // create target + Target target = new Target("4712"); + target = targetManagement.createTarget(target); + final List targets = new ArrayList(); + targets.add(target); + + // create ds + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + // create artifact + final byte random[] = RandomUtils.nextBytes(5 * 1024 * 1024); + final Artifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + ds.findFirstModuleByType(osType).getId(), "file1", false); + + // download fails as artifact is not yet assigned to target + mvc.perform(get("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), + "file1.tar.bz2")).andExpect(status().isNotFound()); + + // now assign and download successful + deploymentManagement.assignDistributionSet(ds, targets); + final MvcResult result = mvc + .perform(get("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), + "file1")) + .andExpect(status().isOk()).andExpect(header().string("ETag", artifact.getSha1Hash())) + .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM)) + .andExpect(header().string("Accept-Ranges", "bytes")) + .andExpect(header().longValue("Last-Modified", artifact.getCreatedAt())) + .andExpect(header().string("Content-Disposition", "attachment;filename=file1")).andReturn(); + + assertTrue(Arrays.equals(result.getResponse().getContentAsByteArray(), random)); + + // one (update) action + assertThat(actionRepository.findByTargetAndDistributionSet(pageReq, target, ds).getContent()).hasSize(1); + final Action action = actionRepository.findByTargetAndDistributionSet(pageReq, target, ds).getContent().get(0); + + // one status - download + assertThat(actionStatusRepository.findAll()).hasSize(2); + assertThat(actionStatusRepository.findByAction(pageReq, action).getContent()).hasSize(2); + assertThat(actionStatusRepository.findByAction(new PageRequest(0, 400, Direction.DESC, "id"), action) + .getContent().get(0).getStatus()).isEqualTo(Status.DOWNLOAD); + + // download complete + assertThat(downLoadProgress).isEqualTo(10); + } + + @Test + @WithUser(principal = "4712", authorities = "ROLE_CONTROLLER", allSpPermissions = true) + @Description("Test various HTTP range requests for artifact download, e.g. chunk download or download resume.") + public void rangeDownloadArtifactByName() throws Exception { + // create target + Target target = new Target("4712"); + target = targetManagement.createTarget(target); + final List targets = new ArrayList<>(); + targets.add(target); + + // create ds + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + final int resultLength = 5 * 1000 * 1024; + + // create artifact + final byte random[] = RandomUtils.nextBytes(resultLength); + final Artifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + ds.findFirstModuleByType(osType).getId(), "file1", false); + + assertThat(random.length).isEqualTo(resultLength); + + // now assign and download successful + deploymentManagement.assignDistributionSet(ds, targets); + + final int range = 100 * 1024; + + // full file download with standard range request + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + for (int i = 0; i < resultLength / range; i++) { + final String rangeString = "" + i * range + "-" + ((i + 1) * range - 1); + + final MvcResult result = mvc + .perform(get("/{tenant}/controller/artifacts/v1/filename/{filename}", + tenantAware.getCurrentTenant(), "file1").header("Range", "bytes=" + rangeString)) + .andExpect(status().isPartialContent()).andExpect(header().string("ETag", artifact.getSha1Hash())) + .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM)) + .andExpect(header().string("Accept-Ranges", "bytes")) + .andExpect(header().longValue("Last-Modified", artifact.getCreatedAt())) + .andExpect(header().longValue("Content-Length", range)) + .andExpect(header().string("Content-Range", "bytes " + rangeString + "/" + resultLength)) + .andExpect(header().string("Content-Disposition", "attachment;filename=file1")).andReturn(); + + outputStream.write(result.getResponse().getContentAsByteArray()); + } + + assertThat(outputStream.toByteArray()).isEqualTo(random); + + // return last 1000 Bytes + MvcResult result = mvc + .perform(get("/${tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), + "file1").header("Range", "bytes=-1000")) + .andExpect(status().isPartialContent()).andExpect(header().string("ETag", artifact.getSha1Hash())) + .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM)) + .andExpect(header().string("Accept-Ranges", "bytes")) + .andExpect(header().longValue("Last-Modified", artifact.getCreatedAt())) + .andExpect(header().longValue("Content-Length", 1000)) + .andExpect(header().string("Content-Range", + "bytes " + (resultLength - 1000) + "-" + (resultLength - 1) + "/" + resultLength)) + .andExpect(header().string("Content-Disposition", "attachment;filename=file1")).andReturn(); + + assertThat(result.getResponse().getContentAsByteArray()) + .isEqualTo(Arrays.copyOfRange(random, resultLength - 1000, resultLength)); + + // skip first 1000 Bytes and return the rest + result = mvc + .perform(get("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), + "file1").header("Range", "bytes=1000-")) + .andExpect(status().isPartialContent()).andExpect(header().string("ETag", artifact.getSha1Hash())) + .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM)) + .andExpect(header().string("Accept-Ranges", "bytes")) + .andExpect(header().longValue("Last-Modified", artifact.getCreatedAt())) + .andExpect(header().longValue("Content-Length", resultLength - 1000)) + .andExpect(header().string("Content-Range", + "bytes " + 1000 + "-" + (resultLength - 1) + "/" + resultLength)) + .andExpect(header().string("Content-Disposition", "attachment;filename=file1")).andReturn(); + + assertThat(result.getResponse().getContentAsByteArray()) + .isEqualTo(Arrays.copyOfRange(random, 1000, resultLength)); + + // multipart download - first 20 bytes in 2 parts + result = mvc + .perform(get("/{tenant}/controller/artifacts/v1/filename/{filename}", tenantAware.getCurrentTenant(), + "file1").header("Range", "bytes=0-9,10-19")) + .andExpect(status().isPartialContent()).andExpect(header().string("ETag", artifact.getSha1Hash())) + .andExpect(content().contentType("multipart/byteranges; boundary=THIS_STRING_SEPARATES_MULTIPART")) + .andExpect(header().string("Accept-Ranges", "bytes")) + .andExpect(header().longValue("Last-Modified", artifact.getCreatedAt())) + .andExpect(header().string("Content-Disposition", "attachment;filename=file1")).andReturn(); + + outputStream.reset(); + + outputStream.write("\r\n--THIS_STRING_SEPARATES_MULTIPART\r\n".getBytes(StandardCharsets.ISO_8859_1)); + outputStream.write(("Content-Range: bytes 0-9/" + resultLength + "\r\n").getBytes(StandardCharsets.ISO_8859_1)); + outputStream.write(Arrays.copyOfRange(random, 0, 10)); + outputStream.write("\r\n--THIS_STRING_SEPARATES_MULTIPART\r\n".getBytes(StandardCharsets.ISO_8859_1)); + outputStream + .write(("Content-Range: bytes 10-19/" + resultLength + "\r\n").getBytes(StandardCharsets.ISO_8859_1)); + outputStream.write(Arrays.copyOfRange(random, 10, 20)); + outputStream.write("\r\n--THIS_STRING_SEPARATES_MULTIPART--".getBytes(StandardCharsets.ISO_8859_1)); + + assertThat(result.getResponse().getContentAsByteArray()).isEqualTo(outputStream.toByteArray()); + + } + + @Test + @Description("Ensures that the download fails if te controller is not authenticated.") + public void faildDownloadArtifactByNameIfAuthenticationMissing() throws Exception { + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + assertThat(artifactRepository.findAll()).hasSize(0); + + // create target + Target target = new Target("4712"); + target = targetManagement.createTarget(target); + final List targets = new ArrayList<>(); + targets.add(target); + + // create ds + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + // create artifact + final byte random[] = RandomUtils.nextBytes(5 * 1024); + final Artifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + ds.findFirstModuleByType(osType).getId(), "file1.tar.bz2", false); + + // download fails as artifact is not yet assigned to target + mvc.perform(get("/controller/artifacts/v1/filename/{filename}", "file1.tar.bz2")) + .andExpect(status().isNotFound()); + } + + @Test + @Description("Downloads an MD5SUM file by the related artifacts filename.") + public void downloadMd5sumFileByName() throws Exception { + // create target + Target target = new Target("4712"); + target = targetManagement.createTarget(target); + + // create ds + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + // create artifact + final byte random[] = RandomUtils.nextBytes(5 * 1024); + final Artifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + ds.findFirstModuleByType(osType).getId(), "file1.tar.bz2", false); + + // download + final MvcResult result = mvc + .perform(get("/{tenant}/controller/artifacts/v1/filename/file1.tar.bz2.MD5SUM", + tenantAware.getCurrentTenant())) + .andExpect(status().isOk()) + .andExpect(header().string("Content-Disposition", "attachment;filename=file1.tar.bz2.MD5SUM")) + .andReturn(); + + assertThat(result.getResponse().getContentAsByteArray()) + .isEqualTo(new String(artifact.getMd5Hash() + " file1.tar.bz2").getBytes(Charsets.US_ASCII)); + } + + @Subscribe + public void listen(final DownloadProgressEvent event) { + downLoadProgress++; + } +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/controller/CancelActionTest.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/controller/CancelActionTest.java new file mode 100755 index 000000000..267b95408 --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/controller/CancelActionTest.java @@ -0,0 +1,549 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.startsWith; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.MockMvcResultPrinter; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.rest.resource.JsonBuilder; +import org.junit.Test; +import org.springframework.hateoas.MediaTypes; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +@ActiveProfiles({ "im", "test" }) +@Features("Component Tests - Controller RESTful API") +@Stories("Cancel Action Resource") +public class CancelActionTest extends AbstractIntegrationTest { + + @Test + @Description("Test of the controller can continue a started update even after a cancel command if it so desires.") + public void rootRsCancelActionButContinueAnyway() throws Exception { + // prepare test data + final Target target = new Target("4712"); + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + final Target savedTarget = targetManagement.createTarget(target); + + final List toAssign = new ArrayList(); + toAssign.add(savedTarget); + + final Action updateAction = deploymentManagement.assignDistributionSet(ds, toAssign).getActions().get(0); + + final Action cancelAction = deploymentManagement.cancelAction(updateAction, + targetManagement.findTargetByControllerID(savedTarget.getControllerId())); + + // controller rejects cancelation + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.cancelActionFeedback(cancelAction.getId().toString(), "rejected")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + final long current = System.currentTimeMillis(); + + // get update action anyway + mvc.perform(get("/{tenant}/controller/v1/4712/deploymentBase/" + updateAction.getId(), + tenantAware.getCurrentTenant()).accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$id", equalTo(String.valueOf(updateAction.getId())))) + .andExpect(jsonPath("$deployment.download", equalTo("forced"))) + .andExpect(jsonPath("$deployment.update", equalTo("forced"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==jvm)][0].version", + equalTo(ds.findFirstModuleByType(runtimeType).getVersion()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].version", + equalTo(ds.findFirstModuleByType(osType).getVersion()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==bApp)][0].version", + equalTo(ds.findFirstModuleByType(appType).getVersion()))); + + // and finish it + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/" + updateAction.getId() + "/feedback", + tenantAware.getCurrentTenant()).content( + JsonBuilder.deploymentActionFeedback(updateAction.getId().toString(), "closed", "success")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + // check database after test + assertThat(targetManagement.findTargetByControllerID("4712").getAssignedDistributionSet().getId()) + .isEqualTo(ds.getId()); + assertThat(targetManagement.findTargetByControllerIDWithDetails("4712").getTargetInfo() + .getInstalledDistributionSet().getId()).isEqualTo(ds.getId()); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getInstallationDate()) + .isGreaterThanOrEqualTo(current); + + } + + @Test + @Description("Test for cancel operation of a update action.") + public void rootRsCancelAction() throws Exception { + final Target target = new Target("4712"); + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + final Target savedTarget = targetManagement.createTarget(target); + + final List toAssign = new ArrayList(); + toAssign.add(savedTarget); + + final Action updateAction = deploymentManagement.assignDistributionSet(ds, toAssign).getActions().get(0); + + long current = System.currentTimeMillis(); + mvc.perform(get("/{tenant}/controller/v1/4712", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaTypes.HAL_JSON)) + .andExpect(jsonPath("$config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$_links.deploymentBase.href", + startsWith("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4712/deploymentBase/" + updateAction.getId()))); + Thread.sleep(1); // is required: otherwise processing the next line is + // often too fast and + // the following assert will fail + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isLessThanOrEqualTo(System.currentTimeMillis()); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + + // Retrieved is reported + + List activeActionsByTarget = deploymentManagement.findActiveActionsByTarget(savedTarget); + + assertThat(activeActionsByTarget).hasSize(1); + assertThat(activeActionsByTarget.get(0).getStatus()).isEqualTo(Status.RUNNING); + final Action cancelAction = deploymentManagement.cancelAction(updateAction, + targetManagement.findTargetByControllerID(savedTarget.getControllerId())); + + activeActionsByTarget = deploymentManagement.findActiveActionsByTarget(savedTarget); + + // the canceled action should still be active! + assertThat(cancelAction.isActive()).isTrue(); + assertThat(activeActionsByTarget).hasSize(1); + assertThat(activeActionsByTarget.get(0).getStatus()).isEqualTo(Status.CANCELING); + + current = System.currentTimeMillis(); + mvc.perform(get("/{tenant}/controller/v1/4712", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaTypes.HAL_JSON)) + .andExpect(jsonPath("$config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$_links.cancelAction.href", + equalTo("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4712/cancelAction/" + cancelAction.getId()))); + Thread.sleep(1); // is required: otherwise processing the next line is + // often too fast and + // the following assert will fail + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isLessThanOrEqualTo(System.currentTimeMillis()); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + + current = System.currentTimeMillis(); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isLessThanOrEqualTo(System.currentTimeMillis()); + mvc.perform( + get("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId(), tenantAware.getCurrentTenant()) + .accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$id", equalTo(String.valueOf(cancelAction.getId())))) + .andExpect(jsonPath("$cancelAction.stopId", equalTo(String.valueOf(updateAction.getId())))); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isLessThanOrEqualTo(System.currentTimeMillis()); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + + // controller confirmed cancelled action, should not be active anymore + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()).accept(MediaType.APPLICATION_JSON) + .content(JsonBuilder.cancelActionFeedback(updateAction.getId().toString(), "closed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + activeActionsByTarget = deploymentManagement.findActiveActionsByTarget(savedTarget); + assertThat(activeActionsByTarget).hasSize(0); + final Action canceledAction = deploymentManagement.findAction(cancelAction.getId()); + assertThat(canceledAction.isActive()).isFalse(); + assertThat(canceledAction.getStatus()).isEqualTo(Status.CANCELED); + + } + + @Test + @Description("Tests various bad requests and if the server handles them as expected.") + public void badCancelAction() throws Exception { + + // not allowed methods + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/1", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + + mvc.perform(put("/{tenant}/controller/v1/4712/cancelAction/1", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete("/{tenant}/controller/v1/4712/cancelAction/1", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + + // non existing target + mvc.perform(get("/{tenant}/controller/v1/34534543/cancelAction/1", tenantAware.getCurrentTenant()) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + createCancelAction("34534543"); + + // wrong media type + mvc.perform(get("/{tenant}/controller/v1/34534543/cancelAction/1", tenantAware.getCurrentTenant()) + .accept(MediaType.APPLICATION_ATOM_XML)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotAcceptable()); + + } + + private Action createCancelAction(final String targetid) { + final Target target = new Target(targetid); + final DistributionSet ds = TestDataUtil.generateDistributionSet(targetid, softwareManagement, + distributionSetManagement); + final Target savedTarget = targetManagement.createTarget(target); + final List toAssign = new ArrayList(); + toAssign.add(savedTarget); + final Action updateAction = deploymentManagement.assignDistributionSet(ds, toAssign).getActions().get(0); + + return deploymentManagement.cancelAction(updateAction, + targetManagement.findTargetByControllerID(savedTarget.getControllerId())); + } + + @Test + @Description("Tests the feedback channel of the cancel operation.") + public void rootRsCancelActionFeedback() throws Exception { + + final Target target = new Target("4712"); + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + final Target savedTarget = targetManagement.createTarget(target); + + final Action updateAction = deploymentManagement.assignDistributionSet(ds.getId(), new String[] { "4712" }) + .getActions().get(0); + + // cancel action manually + final Action cancelAction = deploymentManagement.cancelAction(updateAction, + targetManagement.findTargetByControllerID(savedTarget.getControllerId())); + assertThat(actionStatusRepository.findAll()).hasSize(2); + + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(1); + long current = System.currentTimeMillis(); + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.cancelActionFeedback(cancelAction.getId().toString(), "proceeding")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(1); + assertThat(actionStatusRepository.findAll()).hasSize(3); + + current = System.currentTimeMillis(); + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.cancelActionFeedback(cancelAction.getId().toString(), "resumed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(1); + assertThat(actionStatusRepository.findAll()).hasSize(4); + + current = System.currentTimeMillis(); + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.cancelActionFeedback(cancelAction.getId().toString(), "scheduled")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + assertThat(actionStatusRepository.findAll()).hasSize(5); + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(1); + + // cancelation canceled -> should remove the action from active + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(1); + current = System.currentTimeMillis(); + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.cancelActionFeedback(cancelAction.getId().toString(), "canceled")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + assertThat(actionStatusRepository.findAll()).hasSize(6); + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(1); + + // cancelation rejected -> action still active until controller close it + // with finished or + // error + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(1); + current = System.currentTimeMillis(); + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.cancelActionFeedback(cancelAction.getId().toString(), "rejected")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + assertThat(actionStatusRepository.findAll()).hasSize(7); + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(1); + + // cancelaction closed -> should remove the action from active + current = System.currentTimeMillis(); + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.cancelActionFeedback(cancelAction.getId().toString(), "closed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + assertThat(actionStatusRepository.findAll()).hasSize(8); + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(0); + } + + @Test + @Description("Tests the feeback chanel of for multiple open cancel operations on the same target.") + public void multipleCancelActionFeedback() throws Exception { + final Target target = new Target("4712"); + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement, true); + final DistributionSet ds2 = TestDataUtil.generateDistributionSet("2", softwareManagement, + distributionSetManagement, true); + final DistributionSet ds3 = TestDataUtil.generateDistributionSet("3", softwareManagement, + distributionSetManagement, true); + + final Target savedTarget = targetManagement.createTarget(target); + + final Action updateAction = deploymentManagement.assignDistributionSet(ds.getId(), new String[] { "4712" }) + .getActions().get(0); + final Action updateAction2 = deploymentManagement.assignDistributionSet(ds2.getId(), new String[] { "4712" }) + .getActions().get(0); + final Action updateAction3 = deploymentManagement.assignDistributionSet(ds3.getId(), new String[] { "4712" }) + .getActions().get(0); + + assertThat(actionStatusRepository.findAll()).hasSize(3); + + // 3 update actions, 0 cancel actions + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(3); + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(3); + final Action cancelAction = deploymentManagement.cancelAction(updateAction, + targetManagement.findTargetByControllerID(savedTarget.getControllerId())); + final Action cancelAction2 = deploymentManagement.cancelAction(updateAction2, + targetManagement.findTargetByControllerID(savedTarget.getControllerId())); + + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(3); + assertThat(deploymentManagement.findActionsByTarget(savedTarget)).hasSize(3); + mvc.perform( + get("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId(), tenantAware.getCurrentTenant()) + .accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$id", equalTo(String.valueOf(cancelAction.getId())))) + .andExpect(jsonPath("$cancelAction.stopId", equalTo(String.valueOf(updateAction.getId())))); + assertThat(actionStatusRepository.findAll()).hasSize(6); + + mvc.perform(get("/{tenant}/controller/v1/4712", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaTypes.HAL_JSON)) + .andExpect(jsonPath("$config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$_links.cancelAction.href", + equalTo("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4712/cancelAction/" + cancelAction.getId()))); + + // now lets return feedback for the first cancelation + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.cancelActionFeedback(cancelAction.getId().toString(), "closed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + assertThat(actionStatusRepository.findAll()).hasSize(7); + + // 1 update actions, 1 cancel actions + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(2); + assertThat(deploymentManagement.findActionsByTarget(savedTarget)).hasSize(3); + mvc.perform(get("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction2.getId(), + tenantAware.getCurrentTenant()).accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$id", equalTo(String.valueOf(cancelAction2.getId())))) + .andExpect(jsonPath("$cancelAction.stopId", equalTo(String.valueOf(updateAction2.getId())))); + assertThat(actionStatusRepository.findAll()).hasSize(8); + + mvc.perform(get("/{tenant}/controller/v1/4712", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaTypes.HAL_JSON)) + .andExpect(jsonPath("$config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$_links.cancelAction.href", + equalTo("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4712/cancelAction/" + cancelAction2.getId()))); + + // now lets return feedback for the second cancelation + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction2.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.cancelActionFeedback(cancelAction2.getId().toString(), "closed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + assertThat(actionStatusRepository.findAll()).hasSize(9); + + assertThat(targetManagement.findTargetByControllerID("4712").getAssignedDistributionSet()).isEqualTo(ds3); + mvc.perform(get("/{tenant}/controller/v1/4712/deploymentBase/" + updateAction3.getId(), + tenantAware.getCurrentTenant())).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + assertThat(actionStatusRepository.findAll()).hasSize(10); + + // 1 update actions, 0 cancel actions + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(1); + + final Action cancelAction3 = deploymentManagement.cancelAction(actionRepository.findOne(updateAction3.getId()), + targetManagement.findTargetByControllerID(savedTarget.getControllerId())); + + // action is in cancelling state + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(1); + assertThat(deploymentManagement.findActionsByTarget(savedTarget)).hasSize(3); + assertThat(targetManagement.findTargetByControllerID("4712").getAssignedDistributionSet()).isEqualTo(ds3); + + mvc.perform(get("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction3.getId(), + tenantAware.getCurrentTenant()).accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$id", equalTo(String.valueOf(cancelAction3.getId())))) + .andExpect(jsonPath("$cancelAction.stopId", equalTo(String.valueOf(updateAction3.getId())))); + assertThat(actionStatusRepository.findAll()).hasSize(12); + + // now lets return feedback for the third cancelation + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction3.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.cancelActionFeedback(cancelAction3.getId().toString(), "closed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + assertThat(actionStatusRepository.findAll()).hasSize(13); + + // final status + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(0); + assertThat(deploymentManagement.findActionsByTarget(savedTarget)).hasSize(3); + } + + @Test + @Description("Tests the feeback channel closing for too many feedbacks, i.e. denial of service prevention.") + public void tooMuchCancelActionFeedback() throws Exception { + final Target target = targetManagement.createTarget(new Target("4712")); + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + final List toAssign = new ArrayList(); + toAssign.add(target); + + final Action action = deploymentManagement.assignDistributionSet(ds.getId(), new String[] { "4712" }) + .getActions().get(0); + + final Action cancelAction = deploymentManagement.cancelAction(action, + targetManagement.findTargetByControllerID(target.getControllerId())); + + final String feedback = JsonBuilder.cancelActionFeedback(cancelAction.getId().toString(), "proceeding"); + // assignDistributionSet creates an ActionStatus and cancel action + // stores an action status, so + // only 97 action status left + for (int i = 0; i < 98; i++) { + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()).content(feedback).contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()).content(feedback).contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } + + @Test + @Description("test the correct rejection of various invalid feedback requests") + public void badCancelActionFeedback() throws Exception { + final Action cancelAction = createCancelAction("4712"); + final Action cancelAction2 = createCancelAction("4715"); + + // not allowed methods + mvc.perform(put("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.cancelActionFeedback(cancelAction.getId().toString(), "closed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + mvc.perform(get("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + // bad content type + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.cancelActionFeedback(cancelAction.getId().toString(), "closed")) + .contentType(MediaType.APPLICATION_ATOM_XML).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isUnsupportedMediaType()); + + // bad body + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.cancelActionFeedback(cancelAction.getId().toString(), "546456456")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()); + + // non existing target + mvc.perform(post("/{tenant}/controller/v1/12345/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.cancelActionFeedback(cancelAction.getId().toString(), "closed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + // invalid action + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()).content(JsonBuilder.cancelActionFeedback("sdfsdfsdfs", "closed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()); + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()).content(JsonBuilder.cancelActionFeedback("1234", "closed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + // right action but for wrong target + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.cancelActionFeedback(cancelAction2.getId().toString(), "closed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + // finally get it right :) + mvc.perform(post("/{tenant}/controller/v1/4712/cancelAction/" + cancelAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.cancelActionFeedback(cancelAction.getId().toString(), "closed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + } +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/controller/ConfigDataTest.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/controller/ConfigDataTest.java new file mode 100755 index 000000000..4b6a135b2 --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/controller/ConfigDataTest.java @@ -0,0 +1,181 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.MockMvcResultPrinter; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.rest.resource.JsonBuilder; +import org.junit.Test; +import org.springframework.hateoas.MediaTypes; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +@ActiveProfiles({ "im", "test" }) +@Features("Component Tests - Controller RESTful API") +@Stories("Config Data Resource") +public class ConfigDataTest extends AbstractIntegrationTest { + + @Test + @Description("We verify that the config data (i.e. device attributes like serial number, hardware revision etc.) " + + "are requested only once from the device.") + public void requestConfigDataIfEmpty() throws Exception { + final Target target = new Target("4712"); + final Target savedTarget = targetManagement.createTarget(target); + + final long current = System.currentTimeMillis(); + mvc.perform(get("/{tenant}/controller/v1/4712", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaTypes.HAL_JSON)) + .andExpect(jsonPath("$config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$_links.configData.href", equalTo( + "http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/4712/configData"))); + Thread.sleep(1); // is required: otherwise processing the next line is + // often too fast and + // the following assert will fail + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isLessThanOrEqualTo(System.currentTimeMillis()); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + + savedTarget.getTargetInfo().getControllerAttributes().put("dsafsdf", "sdsds"); + + final Target updateControllerAttributes = controllerManagament.updateControllerAttributes( + savedTarget.getControllerId(), savedTarget.getTargetInfo().getControllerAttributes()); + // request controller attributes need to be false because we don't want + // to request the + // controller attributes again + assertThat(updateControllerAttributes.getTargetInfo().isRequestControllerAttributes()).isFalse(); + + mvc.perform(get("/{tenant}/controller/v1/4712", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaTypes.HAL_JSON)) + .andExpect(jsonPath("$config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$_links.configData.href").doesNotExist()); + } + + @Test + @Description("We verify that the config data (i.e. device attributes like serial number, hardware revision etc.) " + + "can be uploaded correctly by the controller.") + public void putConfigData() throws Exception { + targetManagement.createTarget(new Target("4717")); + + // initial + final Map attributes = new HashMap<>(); + attributes.put("dsafsdf", "sdsds"); + + long current = System.currentTimeMillis(); + mvc.perform(put("/{tenant}/controller/v1/4717/configData", tenantAware.getCurrentTenant()) + .content(JsonBuilder.configData("", attributes, "closed")).contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + Thread.sleep(1); // is required: otherwise processing the next line is + // often too fast and + // the following assert will fail + assertThat(targetManagement.findTargetByControllerID("4717").getTargetInfo().getLastTargetQuery()) + .isLessThanOrEqualTo(System.currentTimeMillis()); + assertThat(targetManagement.findTargetByControllerID("4717").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + assertThat( + targetManagement.findTargetByControllerIDWithDetails("4717").getTargetInfo().getControllerAttributes()) + .isEqualTo(attributes); + + // update + attributes.put("sdsds", "123412"); + current = System.currentTimeMillis(); + mvc.perform(put("/{tenant}/controller/v1/4717/configData", tenantAware.getCurrentTenant()) + .content(JsonBuilder.configData("", attributes, "closed")).contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + Thread.sleep(1); // is required: otherwise processing the next line is + // often too fast and + // the following assert will fail + assertThat(targetManagement.findTargetByControllerID("4717").getTargetInfo().getLastTargetQuery()) + .isLessThanOrEqualTo(System.currentTimeMillis()); + assertThat(targetManagement.findTargetByControllerID("4717").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + assertThat( + targetManagement.findTargetByControllerIDWithDetails("4717").getTargetInfo().getControllerAttributes()) + .isEqualTo(attributes); + } + + @Test + @Description("We verify that the config data (i.e. device attributes like serial number, hardware revision etc.) " + + "upload limitation is inplace which is meant to protect the server from malicious attempts.") + public void putToMuchConfigData() throws Exception { + targetManagement.createTarget(new Target("4717")); + + // initial + Map attributes = new HashMap<>(); + for (int i = 0; i < 10; i++) { + attributes.put("dsafsdf" + i, "sdsds" + i); + } + mvc.perform(put("/{tenant}/controller/v1/4717/configData", tenantAware.getCurrentTenant()) + .content(JsonBuilder.configData("", attributes, "closed")).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + attributes = new HashMap<>(); + attributes.put("on too many", "sdsds"); + mvc.perform(put("/{tenant}/controller/v1/4717/configData", tenantAware.getCurrentTenant()) + .content(JsonBuilder.configData("", attributes, "closed")).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + + } + + @Test + @Description("We verify that the config data (i.e. device attributes like serial number, hardware revision etc.) " + + "resource behaves as exptected in cae of invalid request attempts.") + public void badConfigData() throws Exception { + final Target target = new Target("4712"); + final Target savedTarget = targetManagement.createTarget(target); + + // not allowed methods + mvc.perform(post("/{tenant}/controller/v1/4712/configData", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).// + andExpect(status().isMethodNotAllowed()); + + mvc.perform(get("/{tenant}/controller/v1/4712/configData", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete("/{tenant}/controller/v1/4712/configData", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + + // bad content type + final Map attributes = new HashMap<>(); + attributes.put("dsafsdf", "sdsds"); + mvc.perform(put("/{tenant}/controller/v1/4712/configData", tenantAware.getCurrentTenant()) + .content(JsonBuilder.configData("", attributes, "closed")).contentType(MediaTypes.HAL_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isUnsupportedMediaType()); + + // non existing target + mvc.perform(put("/{tenant}/controller/v1/456456/configData", tenantAware.getCurrentTenant()) + .content(JsonBuilder.configData("", attributes, "closed")).contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + // bad body + mvc.perform(put("/{tenant}/controller/v1/4712/configData", tenantAware.getCurrentTenant()) + .content("{\"id\": \"51659181\"}").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()); + } +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/controller/DeploymentBaseTest.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/controller/DeploymentBaseTest.java new file mode 100755 index 000000000..cb3a684fb --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/controller/DeploymentBaseTest.java @@ -0,0 +1,984 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.startsWith; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.RandomUtils; +import org.eclipse.hawkbit.AbstractIntegrationTestWithMongoDB; +import org.eclipse.hawkbit.MockMvcResultPrinter; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.eclipse.hawkbit.rest.resource.JsonBuilder; +import org.fest.assertions.core.Condition; +import org.junit.Test; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.hateoas.MediaTypes; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +@ActiveProfiles({ "im", "test" }) +@Features("Component Tests - Controller RESTful API") +@Stories("Deployment Action Resource") +public class DeploymentBaseTest extends AbstractIntegrationTestWithMongoDB { + + @Test() + @Description("Ensures that artifact not found, when softare module not exists ") + public void testArtifactsNotFound() throws Exception { + final Target target = TestDataUtil.createTarget(targetManagement); + final Long softwareModuleIdNotExist = 1l; + mvc.perform(get("/{tenant}/controller/v1/{targetNotExist}/softwaremodules/{softwareModuleId}/artifacts", + tenantAware.getCurrentTenant(), target.getName(), softwareModuleIdNotExist)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + } + + @Test() + @Description("Ensures that artifact are found, when softare module exists ") + public void testArtifactsExists() throws Exception { + final Target target = TestDataUtil.createTarget(targetManagement); + final DistributionSet distributionSet = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + deploymentManagement.assignDistributionSet(distributionSet.getId(), new String[] { target.getName() }); + + final Long softwareModuleId = distributionSet.getModules().stream().findFirst().get().getId(); + mvc.perform(get("/{tenant}/controller/v1/{targetNotExist}/softwaremodules/{softwareModuleId}/artifacts", + tenantAware.getCurrentTenant(), target.getName(), softwareModuleId)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("$", hasSize(0))); + + TestDataUtil.generateArtifacts(artifactManagement, softwareModuleId); + + mvc.perform(get("/{tenant}/controller/v1/{targetNotExist}/softwaremodules/{softwareModuleId}/artifacts", + tenantAware.getCurrentTenant(), target.getName(), softwareModuleId)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("$", hasSize(3))) + .andExpect(jsonPath("$[?(@.filename==filename0)]", hasSize(1))) + .andExpect(jsonPath("$[?(@.filename==filename1)]", hasSize(1))) + .andExpect(jsonPath("$[?(@.filename==filename2)]", hasSize(1))); + + } + + @Test() + @Description("Ensures that software modules are found, when assigned distribution set exists with modules and atrifacts") + public void testAvailableSoftwareModulesWithArtifacts() throws Exception { + final Target target = TestDataUtil.createTarget(targetManagement); + final DistributionSet distributionSet = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + deploymentManagement.assignDistributionSet(distributionSet.getId(), new String[] { target.getName() }); + final Long softwareModuleId = distributionSet.getModules().stream().findFirst().get().getId(); + TestDataUtil.generateArtifacts(artifactManagement, softwareModuleId); + + final int modulesSize = distributionSet.getModules().size(); + + mvc.perform(get("/{tenant}/controller/v1/{targetExist}/softwaremodules/", tenantAware.getCurrentTenant(), + target.getName())).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(modulesSize))); + } + + @Test + @Description("Forced deployment to a controller. Checks if the resource reponse payload for a given deployment is as expected.") + public void deplomentForceAction() throws Exception { + // Prepare test data + final Target target = new Target("4712"); + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement, true); + final DistributionSet ds2 = TestDataUtil.generateDistributionSet("2", softwareManagement, + distributionSetManagement, true); + + final byte random[] = RandomUtils.nextBytes(5 * 1024); + final LocalArtifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + ds.findFirstModuleByType(osType).getId(), "test1", false); + final LocalArtifact artifactSignature = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + ds.findFirstModuleByType(osType).getId(), "test1.signature", false); + + final Target savedTarget = targetManagement.createTarget(target); + + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).isEmpty(); + assertThat(actionRepository.findAll()).isEmpty(); + assertThat(actionStatusRepository.findAll()).isEmpty(); + + List saved = deploymentManagement.assignDistributionSet(ds.getId(), ActionType.FORCED, + Action.NO_FORCE_TIME, savedTarget.getControllerId()).getAssignedTargets(); + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(1); + + final Action action = deploymentManagement.findActiveActionsByTarget(savedTarget).get(0); + assertThat(actionRepository.findAll()).hasSize(1); + saved = deploymentManagement.assignDistributionSet(ds2, saved).getAssignedTargets(); + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(2); + assertThat(actionRepository.findAll()).hasSize(2); + + final Action uaction = deploymentManagement.findActiveActionsByTarget(savedTarget).get(0); + assertThat(uaction.getDistributionSet()).isEqualTo(ds); + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(2); + + // Run test + long current = System.currentTimeMillis(); + mvc.perform(get("/{tenant}/controller/v1/4712", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaTypes.HAL_JSON)) + .andExpect(jsonPath("$config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$_links.deploymentBase.href", startsWith("http://localhost/" + + tenantAware.getCurrentTenant() + "/controller/v1/4712/deploymentBase/" + uaction.getId()))); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isLessThanOrEqualTo(System.currentTimeMillis()); + assertThat(actionStatusRepository.findAll()).hasSize(2); + + current = System.currentTimeMillis(); + Thread.sleep(1); + + final DistributionSet findDistributionSetByAction = distributionSetManagement + .findDistributionSetByAction(action); + + mvc.perform( + get("/{tenant}/controller/v1/4712/deploymentBase/" + uaction.getId(), tenantAware.getCurrentTenant()) + .accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$id", equalTo(String.valueOf(action.getId())))) + .andExpect(jsonPath("$deployment.download", equalTo("forced"))) + .andExpect(jsonPath("$deployment.update", equalTo("forced"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==jvm)][0].name", + equalTo(ds.findFirstModuleByType(runtimeType).getName()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==jvm)][0].version", + equalTo(ds.findFirstModuleByType(runtimeType).getVersion()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].name", + equalTo(ds.findFirstModuleByType(osType).getName()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].version", + equalTo(ds.findFirstModuleByType(osType).getVersion()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0].size", equalTo(5 * 1024))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0].filename", equalTo("test1"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0].hashes.md5", + equalTo(artifact.getMd5Hash()))) + .andExpect( + jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0].hashes.sha1", + equalTo(artifact.getSha1Hash()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0]._links.download.href", + equalTo("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4712/softwaremodules/" + + findDistributionSetByAction.findFirstModuleByType(osType).getId() + + "/artifacts/test1"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0]._links.md5sum.href", + equalTo("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4712/softwaremodules/" + + findDistributionSetByAction.findFirstModuleByType(osType).getId() + + "/artifacts/test1.MD5SUM"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1].size", equalTo(5 * 1024))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1].filename", + equalTo("test1.signature"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1].hashes.md5", + equalTo(artifactSignature.getMd5Hash()))) + .andExpect( + jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1].hashes.sha1", + equalTo(artifactSignature.getSha1Hash()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1]._links.download.href", + equalTo("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4712/softwaremodules/" + + findDistributionSetByAction.findFirstModuleByType(osType).getId() + + "/artifacts/test1.signature"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1]._links.md5sum.href", + equalTo("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4712/softwaremodules/" + + findDistributionSetByAction.findFirstModuleByType(osType).getId() + + "/artifacts/test1.signature.MD5SUM"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==bApp)][0].version", + equalTo(ds.findFirstModuleByType(appType).getVersion()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==bApp)][0].name", + equalTo(ds.findFirstModuleByType(appType).getName()))); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + + // Retrieved is reported + final Iterable actionStatusMessages = actionStatusRepository + .findAll(new PageRequest(0, 100, Direction.DESC, "id")); + assertThat(actionStatusMessages).hasSize(3); + final ActionStatus actionStatusMessage = actionStatusMessages.iterator().next(); + assertThat(actionStatusMessage.getStatus()).isEqualTo(Status.RETRIEVED); + } + + @Test + @Description("Attempt/soft deployment to a controller. Checks if the resource reponse payload for a given deployment is as expected.") + public void deplomentAttemptAction() throws Exception { + // Prepare test data + final Target target = new Target("4712"); + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement, true); + final DistributionSet ds2 = TestDataUtil.generateDistributionSet("2", softwareManagement, + distributionSetManagement, true); + + final byte random[] = RandomUtils.nextBytes(5 * 1024); + final LocalArtifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + ds.findFirstModuleByType(osType).getId(), "test1", false); + final LocalArtifact artifactSignature = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + ds.findFirstModuleByType(osType).getId(), "test1.signature", false); + + final Target savedTarget = targetManagement.createTarget(target); + + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).isEmpty(); + assertThat(actionRepository.findAll()).isEmpty(); + assertThat(actionStatusRepository.findAll()).isEmpty(); + + List saved = deploymentManagement + .assignDistributionSet(ds.getId(), ActionType.SOFT, Action.NO_FORCE_TIME, savedTarget.getControllerId()) + .getAssignedTargets(); + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(1); + + final Action action = deploymentManagement.findActiveActionsByTarget(savedTarget).get(0); + assertThat(actionRepository.findAll()).hasSize(1); + saved = deploymentManagement.assignDistributionSet(ds2, saved).getAssignedTargets(); + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(2); + assertThat(actionRepository.findAll()).hasSize(2); + + final Action uaction = deploymentManagement.findActiveActionsByTarget(savedTarget).get(0); + assertThat(uaction.getDistributionSet()).isEqualTo(ds); + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(2); + + // Run test + + long current = System.currentTimeMillis(); + mvc.perform(get("/{tenant}/controller/v1/4712", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaTypes.HAL_JSON)) + .andExpect(jsonPath("$config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$_links.deploymentBase.href", startsWith("http://localhost/" + + tenantAware.getCurrentTenant() + "/controller/v1/4712/deploymentBase/" + uaction.getId()))); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isLessThanOrEqualTo(System.currentTimeMillis()); + assertThat(actionStatusRepository.findAll()).hasSize(2); + + current = System.currentTimeMillis(); + Thread.sleep(1); + + final DistributionSet findDistributionSetByAction = distributionSetManagement + .findDistributionSetByAction(action); + + mvc.perform( + get("/{tenant}/controller/v1/4712/deploymentBase/" + uaction.getId(), tenantAware.getCurrentTenant()) + .accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$id", equalTo(String.valueOf(action.getId())))) + .andExpect(jsonPath("$deployment.download", equalTo("attempt"))) + .andExpect(jsonPath("$deployment.update", equalTo("attempt"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==jvm)][0].name", + equalTo(ds.findFirstModuleByType(runtimeType).getName()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==jvm)][0].version", + equalTo(ds.findFirstModuleByType(runtimeType).getVersion()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].name", + equalTo(ds.findFirstModuleByType(osType).getName()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].version", + equalTo(ds.findFirstModuleByType(osType).getVersion()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0].size", equalTo(5 * 1024))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0].filename", equalTo("test1"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0].hashes.md5", + equalTo(artifact.getMd5Hash()))) + .andExpect( + jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0].hashes.sha1", + equalTo(artifact.getSha1Hash()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0]._links.download.href", + equalTo("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4712/softwaremodules/" + + findDistributionSetByAction.findFirstModuleByType(osType).getId() + + "/artifacts/test1"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0]._links.md5sum.href", + equalTo("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4712/softwaremodules/" + + findDistributionSetByAction.findFirstModuleByType(osType).getId() + + "/artifacts/test1.MD5SUM"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1].size", equalTo(5 * 1024))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1].filename", + equalTo("test1.signature"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1].hashes.md5", + equalTo(artifactSignature.getMd5Hash()))) + .andExpect( + jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1].hashes.sha1", + equalTo(artifactSignature.getSha1Hash()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1]._links.download.href", + equalTo("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4712/softwaremodules/" + + findDistributionSetByAction.findFirstModuleByType(osType).getId() + + "/artifacts/test1.signature"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1]._links.md5sum.href", + equalTo("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4712/softwaremodules/" + + findDistributionSetByAction.findFirstModuleByType(osType).getId() + + "/artifacts/test1.signature.MD5SUM"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==bApp)][0].version", + equalTo(ds.findFirstModuleByType(appType).getVersion()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==bApp)][0].name", + equalTo(ds.findFirstModuleByType(appType).getName()))); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + + // Retrieved is reported + final Iterable actionStatusMessages = actionStatusRepository + .findAll(new PageRequest(0, 100, Direction.DESC, "id")); + assertThat(actionStatusMessages).hasSize(3); + final ActionStatus actionStatusMessage = actionStatusMessages.iterator().next(); + assertThat(actionStatusMessage.getStatus()).isEqualTo(Status.RETRIEVED); + } + + @Test + @Description("Attempt/soft deployment to a controller including automated switch to hard. Checks if the resource reponse payload for a given deployment is as expected.") + public void deplomentAutoForceAction() throws Exception { + // Prepare test data + final Target target = new Target("4712"); + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement, true); + final DistributionSet ds2 = TestDataUtil.generateDistributionSet("2", softwareManagement, + distributionSetManagement, true); + + final byte random[] = RandomUtils.nextBytes(5 * 1024); + final LocalArtifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + ds.findFirstModuleByType(osType).getId(), "test1", false); + final LocalArtifact artifactSignature = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + ds.findFirstModuleByType(osType).getId(), "test1.signature", false); + + final Target savedTarget = targetManagement.createTarget(target); + + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).isEmpty(); + assertThat(actionRepository.findAll()).isEmpty(); + assertThat(actionStatusRepository.findAll()).isEmpty(); + + List saved = deploymentManagement.assignDistributionSet(ds.getId(), ActionType.TIMEFORCED, + System.currentTimeMillis(), savedTarget.getControllerId()).getAssignedTargets(); + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(1); + + final Action action = deploymentManagement.findActiveActionsByTarget(savedTarget).get(0); + assertThat(actionRepository.findAll()).hasSize(1); + saved = deploymentManagement.assignDistributionSet(ds2, saved).getAssignedTargets(); + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(2); + assertThat(actionRepository.findAll()).hasSize(2); + + final Action uaction = deploymentManagement.findActiveActionsByTarget(savedTarget).get(0); + assertThat(uaction.getDistributionSet()).isEqualTo(ds); + assertThat(deploymentManagement.findActiveActionsByTarget(savedTarget)).hasSize(2); + + // Run test + + long current = System.currentTimeMillis(); + mvc.perform(get("/{tenant}/controller/v1/4712", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaTypes.HAL_JSON)) + .andExpect(jsonPath("$config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$_links.deploymentBase.href", startsWith("http://localhost/" + + tenantAware.getCurrentTenant() + "/controller/v1/4712/deploymentBase/" + uaction.getId()))); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isLessThanOrEqualTo(System.currentTimeMillis()); + assertThat(actionStatusRepository.findAll()).hasSize(2); + + current = System.currentTimeMillis(); + Thread.sleep(1); + + final DistributionSet findDistributionSetByAction = distributionSetManagement + .findDistributionSetByAction(action); + + mvc.perform(get("/{tenant}/controller/v1/4712/deploymentBase/{actionId}", tenantAware.getCurrentTenant(), + uaction.getId()).accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$id", equalTo(String.valueOf(action.getId())))) + .andExpect(jsonPath("$deployment.download", equalTo("forced"))) + .andExpect(jsonPath("$deployment.update", equalTo("forced"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==jvm)][0].name", + equalTo(ds.findFirstModuleByType(runtimeType).getName()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==jvm)][0].version", + equalTo(ds.findFirstModuleByType(runtimeType).getVersion()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].name", + equalTo(ds.findFirstModuleByType(osType).getName()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].version", + equalTo(ds.findFirstModuleByType(osType).getVersion()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0].size", equalTo(5 * 1024))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0].filename", equalTo("test1"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0].hashes.md5", + equalTo(artifact.getMd5Hash()))) + .andExpect( + jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0].hashes.sha1", + equalTo(artifact.getSha1Hash()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0]._links.download.href", + equalTo("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4712/softwaremodules/" + + findDistributionSetByAction.findFirstModuleByType(osType).getId() + + "/artifacts/test1"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[0]._links.md5sum.href", + equalTo("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4712/softwaremodules/" + + findDistributionSetByAction.findFirstModuleByType(osType).getId() + + "/artifacts/test1.MD5SUM"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1].size", equalTo(5 * 1024))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1].filename", + equalTo("test1.signature"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1].hashes.md5", + equalTo(artifactSignature.getMd5Hash()))) + .andExpect( + jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1].hashes.sha1", + equalTo(artifactSignature.getSha1Hash()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1]._links.download.href", + equalTo("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4712/softwaremodules/" + + findDistributionSetByAction.findFirstModuleByType(osType).getId() + + "/artifacts/test1.signature"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==os)][0].artifacts[1]._links.md5sum.href", + equalTo("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4712/softwaremodules/" + + findDistributionSetByAction.findFirstModuleByType(osType).getId() + + "/artifacts/test1.signature.MD5SUM"))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==bApp)][0].version", + equalTo(ds.findFirstModuleByType(appType).getVersion()))) + .andExpect(jsonPath("$deployment.chunks[?(@.part==bApp)][0].name", + equalTo(ds.findFirstModuleByType(appType).getName()))); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + + // Retrieved is reported + final Iterable actionStatusMessages = actionStatusRepository + .findAll(new PageRequest(0, 100, Direction.DESC, "id")); + assertThat(actionStatusMessages).hasSize(3); + final ActionStatus actionStatusMessage = actionStatusMessages.iterator().next(); + assertThat(actionStatusMessage.getStatus()).isEqualTo(Status.RETRIEVED); + } + + @Test + @Description("Test various invalid access attempts to the deployment resource und the expected behaviour of the server.") + public void badDeploymentAction() throws Exception { + final Target target = targetManagement.createTarget(new Target("4712")); + + // not allowed methods + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/1", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + + mvc.perform(put("/{tenant}/controller/v1/4712/deploymentBase/1", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete("/{tenant}/controller/v1/4712/deploymentBase/1", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + + // non existing target + mvc.perform(get("/{tenant}/controller/v1/4715/deploymentBase/1", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + // no deployment + mvc.perform(get("/controller/v1/4712/deploymentBase/1", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + // wrong media type + final List toAssign = new ArrayList(); + toAssign.add(target); + final DistributionSet savedSet = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + final Action action1 = deploymentManagement.assignDistributionSet(savedSet, toAssign).getActions().get(0); + mvc.perform( + get("/{tenant}/controller/v1/4712/deploymentBase/" + action1.getId(), tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + mvc.perform( + get("/{tenant}/controller/v1/4712/deploymentBase/" + action1.getId(), tenantAware.getCurrentTenant()) + .accept(MediaType.APPLICATION_ATOM_XML)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotAcceptable()); + } + + @Test + @Description("The server protects itself against to many feedback upload attempts. The test verfies that " + + "it is not possible to exceed the configured maximum number of feedback uplods.") + public void toMuchDeplomentActionFeedback() throws Exception { + final Target target = targetManagement.createTarget(new Target("4712")); + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + final List toAssign = new ArrayList(); + toAssign.add(target); + + deploymentManagement.assignDistributionSet(ds.getId(), new String[] { "4712" }); + final Pageable pageReq = new PageRequest(0, 100); + final Action action = actionRepository.findByDistributionSet(pageReq, ds).getContent().get(0); + + final String feedback = JsonBuilder.deploymentActionFeedback(action.getId().toString(), "proceeding"); + // assign distribution set creates an action status, so only 99 left + for (int i = 0; i < 99; i++) { + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/" + action.getId() + "/feedback", + tenantAware.getCurrentTenant()).content(feedback).contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/" + action.getId() + "/feedback", + tenantAware.getCurrentTenant()).content(feedback).contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } + + @Test + @Description("Multiple uploads of deployment status feedback to the server.") + public void multipleDeplomentActionFeedback() throws Exception { + final Target target1 = new Target("4712"); + final Target target2 = new Target("4713"); + final Target target3 = new Target("4714"); + final Target savedTarget1 = targetManagement.createTarget(target1); + targetManagement.createTarget(target2); + targetManagement.createTarget(target3); + + final DistributionSet ds1 = TestDataUtil.generateDistributionSet("1", softwareManagement, + distributionSetManagement, true); + final DistributionSet ds2 = TestDataUtil.generateDistributionSet("2", softwareManagement, + distributionSetManagement, true); + final DistributionSet ds3 = TestDataUtil.generateDistributionSet("3", softwareManagement, + distributionSetManagement, true); + + final List toAssign = new ArrayList(); + toAssign.add(savedTarget1); + + final Action action1 = deploymentManagement.assignDistributionSet(ds1.getId(), new String[] { "4712" }) + .getActions().get(0); + final Action action2 = deploymentManagement.assignDistributionSet(ds2.getId(), new String[] { "4712" }) + .getActions().get(0); + final Action action3 = deploymentManagement.assignDistributionSet(ds3.getId(), new String[] { "4712" }) + .getActions().get(0); + + Target myT = targetManagement.findTargetByControllerID("4712"); + assertThat(myT.getTargetInfo().getUpdateStatus()).isEqualTo(TargetUpdateStatus.PENDING); + assertThat(deploymentManagement.findActiveActionsByTarget(myT)).hasSize(3); + assertThat(myT.getAssignedDistributionSet()).isEqualTo(ds3); + assertThat(myT.getTargetInfo().getInstalledDistributionSet()).isNull(); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.UNKNOWN)) + .hasSize(2); + + // action1 done + + long current = System.currentTimeMillis(); + long lastModified = targetManagement.findTargetByControllerID("4712").getLastModifiedAt(); + Thread.sleep(1); // is required: otherwise processing the next line is + // often too fast and + // the following assert will fail + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/" + action1.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(action1.getId().toString(), "closed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + Thread.sleep(1); // is required: otherwise processing the next line is + // often too fast and + // the following assert will fail + myT = targetManagement.findTargetByControllerIDWithDetails("4712"); + assertThat(myT.getTargetInfo().getLastTargetQuery()).isLessThanOrEqualTo(System.currentTimeMillis()); + assertThat(myT.getTargetInfo().getLastTargetQuery()).isGreaterThanOrEqualTo(current); + // assertThat( myT.getLastModifiedAt() ).isEqualTo( lastModified ); + + final long timeDiff = Math + .abs(myT.getTargetInfo().getLastTargetQuery() - myT.getTargetInfo().getInstallationDate()); + + assertThat(myT.getTargetInfo().getUpdateStatus()).isEqualTo(TargetUpdateStatus.PENDING); + assertThat(deploymentManagement.findActiveActionsByTarget(myT)).hasSize(2); + assertThat(myT.getTargetInfo().getInstalledDistributionSet().getId()).isEqualTo(ds1.getId()); + assertThat(myT.getAssignedDistributionSet()).isEqualTo(ds3); + + Iterable actionStatusMessages = actionStatusRepository.findAll(new Sort(Direction.DESC, "id")); + assertThat(actionStatusMessages).hasSize(4); + assertThat(actionStatusMessages.iterator().next().getStatus()).isEqualTo(Status.FINISHED); + + // action2 done + current = System.currentTimeMillis(); + lastModified = targetManagement.findTargetByControllerID("4712").getLastModifiedAt(); + Thread.sleep(1); // is required: otherwise processing the next line is + // often too fast and + // the following assert will fail + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/" + action2.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(action2.getId().toString(), "closed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + Thread.sleep(1); // is required: otherwise processing the next line is + // often too fast and + // the following assert will fail + myT = targetManagement.findTargetByControllerIDWithDetails("4712"); + assertThat(myT.getTargetInfo().getLastTargetQuery()).isLessThanOrEqualTo(System.currentTimeMillis()); + assertThat(myT.getTargetInfo().getLastTargetQuery()).isGreaterThanOrEqualTo(current); + + assertThat(myT.getTargetInfo().getUpdateStatus()).isEqualTo(TargetUpdateStatus.PENDING); + assertThat(deploymentManagement.findActiveActionsByTarget(myT)).hasSize(1); + assertThat(myT.getTargetInfo().getInstalledDistributionSet().getId()).isEqualTo(ds2.getId()); + assertThat(myT.getAssignedDistributionSet()).isEqualTo(ds3); + actionStatusMessages = actionStatusRepository.findAll(new PageRequest(0, 100, Direction.DESC, "id")); + assertThat(actionStatusMessages).hasSize(5); + assertThat(actionStatusMessages).haveAtLeast(1, new ActionStatusCondition(Status.FINISHED)); + + // action3 done + current = System.currentTimeMillis(); + lastModified = targetManagement.findTargetByControllerID("4712").getLastModifiedAt(); + Thread.sleep(1); // is required: otherwise processing the next line is + // often too fast and + // the following assert will fail + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/" + action3.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(action3.getId().toString(), "closed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + Thread.sleep(1); // is required: otherwise processing the next line is + // often too fast and + // the following assert will fail + + myT = targetManagement.findTargetByControllerID("4712"); + assertThat(myT.getTargetInfo().getLastTargetQuery()).isLessThanOrEqualTo(System.currentTimeMillis()); + assertThat(myT.getTargetInfo().getLastTargetQuery()).isGreaterThanOrEqualTo(current); + assertThat(myT.getTargetInfo().getUpdateStatus()).isEqualTo(TargetUpdateStatus.IN_SYNC); + assertThat(deploymentManagement.findActiveActionsByTarget(myT)).hasSize(0); + assertThat(myT.getTargetInfo().getInstalledDistributionSet()).isEqualTo(ds3); + assertThat(myT.getAssignedDistributionSet()).isEqualTo(ds3); + actionStatusMessages = actionStatusRepository.findAll(); + assertThat(actionStatusMessages).hasSize(6); + assertThat(actionStatusMessages).haveAtLeast(1, new ActionStatusCondition(Status.FINISHED)); + + } + + @Test + @Description("Verfies that an update action is correctly set to error if the controller provides error feedback.") + public void rootRsSingleDeplomentActionWithErrorFeedback() throws Exception { + final Target target = new Target("4712"); + DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, distributionSetManagement); + + final Target savedTarget = targetManagement.createTarget(target); + + List toAssign = new ArrayList(); + toAssign.add(savedTarget); + + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.UNKNOWN); + deploymentManagement.assignDistributionSet(ds, toAssign); + final Action action = actionRepository.findByDistributionSet(pageReq, ds).getContent().get(0); + + long current = System.currentTimeMillis(); + long lastModified = targetManagement.findTargetByControllerID("4712").getLastModifiedAt(); + Thread.sleep(1); // is required: otherwise processing the next line is + // often too fast and + // the following assert will fail + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/" + action.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(action.getId().toString(), "closed", "failure")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + Thread.sleep(1); // is required: otherwise processing the next line is + // often too fast and + // the following assert will fail + Target myT = targetManagement.findTargetByControllerID("4712"); + assertThat(myT.getTargetInfo().getLastTargetQuery()).isLessThanOrEqualTo(System.currentTimeMillis()); + assertThat(myT.getTargetInfo().getLastTargetQuery()).isGreaterThanOrEqualTo(current); + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.ERROR); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.PENDING)) + .hasSize(0); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.ERROR)) + .hasSize(1); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.IN_SYNC)) + .hasSize(0); + assertThat(deploymentManagement.findActiveActionsByTarget(myT)).hasSize(0); + assertThat(deploymentManagement.findActionsByTarget(myT)).hasSize(1); + final Iterable actionStatusMessages = actionStatusRepository.findAll(); + assertThat(actionStatusMessages).hasSize(2); + assertThat(actionStatusMessages).haveAtLeast(1, new ActionStatusCondition(Status.ERROR)); + + // redo + toAssign = new ArrayList(); + toAssign.add(targetManagement.findTargetByControllerID("4712")); + ds = distributionSetManagement.findDistributionSetByIdWithDetails(ds.getId()); + deploymentManagement.assignDistributionSet(ds, toAssign); + final Action action2 = deploymentManagement.findActiveActionsByTarget(myT).get(0); + current = System.currentTimeMillis(); + lastModified = targetManagement.findTargetByControllerID("4712").getLastModifiedAt(); + Thread.sleep(1); // is required: otherwise processing the next line is + // often too fast and + // the following assert will fail + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/" + action2.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(action2.getId().toString(), "closed", "success")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + Thread.sleep(1); // is required: otherwise processing the next line is + // often too fast and + // the following assert will fail + myT = targetManagement.findTargetByControllerID("4712"); + assertThat(myT.getTargetInfo().getLastTargetQuery()).isLessThanOrEqualTo(System.currentTimeMillis()); + assertThat(myT.getTargetInfo().getLastTargetQuery()).isGreaterThanOrEqualTo(current); + assertThat(myT.getTargetInfo().getUpdateStatus()).isEqualTo(TargetUpdateStatus.IN_SYNC); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.PENDING)) + .hasSize(0); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.ERROR)) + .hasSize(0); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.IN_SYNC)) + .hasSize(1); + assertThat(deploymentManagement.findActiveActionsByTarget(myT)).hasSize(0); + assertThat(deploymentManagement.findInActiveActionsByTarget(myT)).hasSize(2); + assertThat(actionStatusRepository.findAll()).hasSize(4); + assertThat(actionStatusRepository.findByAction(pageReq, action).getContent()).haveAtLeast(1, + new ActionStatusCondition(Status.ERROR)); + assertThat(actionStatusRepository.findByAction(pageReq, action2).getContent()).haveAtLeast(1, + new ActionStatusCondition(Status.FINISHED)); + + } + + @Test + @Description("Verfies that the controller can provided as much feedback entries as necessry as long as it is in the configured limites.") + public void rootRsSingleDeplomentActionFeedback() throws Exception { + final Target target = new Target("4712"); + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + final Target savedTarget = targetManagement.createTarget(target); + + final List toAssign = new ArrayList(); + toAssign.add(savedTarget); + + Target myT = targetManagement.findTargetByControllerID("4712"); + assertThat(myT.getTargetInfo().getUpdateStatus()).isEqualTo(TargetUpdateStatus.UNKNOWN); + deploymentManagement.assignDistributionSet(ds, toAssign); + final Action action = actionRepository.findByDistributionSet(pageReq, ds).getContent().get(0); + + myT = targetManagement.findTargetByControllerID("4712"); + assertThat(myT.getTargetInfo().getUpdateStatus()).isEqualTo(TargetUpdateStatus.PENDING); + assertThat(targetRepository.findByTargetInfoInstalledDistributionSet(new PageRequest(0, 10), ds)).hasSize(0); + assertThat(targetRepository.findByAssignedDistributionSet(new PageRequest(0, 10), ds)).hasSize(1); + assertThat(targetRepository + .findByAssignedDistributionSetOrTargetInfoInstalledDistributionSet(new PageRequest(0, 10), ds, ds)) + .hasSize(1); + + // Now valid Feedback + + long current = System.currentTimeMillis(); + + for (int i = 0; i < 4; i++) { + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/" + action.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(action.getId().toString(), "proceeding")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + } + + myT = targetManagement.findTargetByControllerID("4712"); + assertThat(myT.getTargetInfo().getLastTargetQuery()).isGreaterThanOrEqualTo(current); + assertThat(myT.getTargetInfo().getUpdateStatus()).isEqualTo(TargetUpdateStatus.PENDING); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.PENDING)) + .hasSize(1); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.ERROR)) + .hasSize(0); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.IN_SYNC)) + .hasSize(0); + assertThat(deploymentManagement.findActiveActionsByTarget(myT)).hasSize(1); + assertThat(actionStatusRepository.findAll()).hasSize(5); + assertThat(actionStatusRepository.findAll()).haveAtLeast(5, new ActionStatusCondition(Status.RUNNING)); + + current = System.currentTimeMillis(); + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/" + action.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(action.getId().toString(), "scheduled")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + myT = targetManagement.findTargetByControllerID("4712"); + assertThat(myT.getTargetInfo().getLastTargetQuery()).isGreaterThanOrEqualTo(current); + assertThat(myT.getTargetInfo().getUpdateStatus()).isEqualTo(TargetUpdateStatus.PENDING); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.PENDING)) + .hasSize(1); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.ERROR)) + .hasSize(0); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.IN_SYNC)) + .hasSize(0); + assertThat(deploymentManagement.findActiveActionsByTarget(myT)).hasSize(1); + assertThat(actionStatusRepository.findAll()).hasSize(6); + assertThat(actionStatusRepository.findAll()).haveAtLeast(5, new ActionStatusCondition(Status.RUNNING)); + + current = System.currentTimeMillis(); + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/" + action.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(action.getId().toString(), "resumed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + myT = targetManagement.findTargetByControllerID("4712"); + assertThat(myT.getTargetInfo().getLastTargetQuery()).isGreaterThanOrEqualTo(current); + assertThat(myT.getTargetInfo().getUpdateStatus()).isEqualTo(TargetUpdateStatus.PENDING); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.PENDING)) + .hasSize(1); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.ERROR)) + .hasSize(0); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.IN_SYNC)) + .hasSize(0); + assertThat(deploymentManagement.findActiveActionsByTarget(myT)).hasSize(1); + assertThat(actionStatusRepository.findAll()).hasSize(7); + assertThat(actionStatusRepository.findAll()).haveAtLeast(6, new ActionStatusCondition(Status.RUNNING)); + + current = System.currentTimeMillis(); + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/" + action.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(action.getId().toString(), "canceled")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + myT = targetManagement.findTargetByControllerID("4712"); + assertThat(myT.getTargetInfo().getLastTargetQuery()).isGreaterThanOrEqualTo(current); + assertThat(myT.getTargetInfo().getUpdateStatus()).isEqualTo(TargetUpdateStatus.PENDING); + assertThat(deploymentManagement.findActiveActionsByTarget(myT)).hasSize(1); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.PENDING)) + .hasSize(1); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.ERROR)) + .hasSize(0); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.IN_SYNC)) + .hasSize(0); + + assertThat(actionStatusRepository.findAll()).hasSize(8); + assertThat(actionStatusRepository.findAll()).haveAtLeast(7, new ActionStatusCondition(Status.RUNNING)); + assertThat(actionStatusRepository.findAll()).haveAtLeast(1, new ActionStatusCondition(Status.CANCELED)); + + current = System.currentTimeMillis(); + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/" + action.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(action.getId().toString(), "rejected")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + myT = targetManagement.findTargetByControllerID("4712"); + assertThat(myT.getTargetInfo().getLastTargetQuery()).isGreaterThanOrEqualTo(current); + assertThat(myT.getTargetInfo().getUpdateStatus()).isEqualTo(TargetUpdateStatus.PENDING); + assertThat(deploymentManagement.findActiveActionsByTarget(myT)).hasSize(1); + assertThat(actionStatusRepository.findAll()).hasSize(9); + assertThat(actionStatusRepository.findAll()).haveAtLeast(6, new ActionStatusCondition(Status.RUNNING)); + assertThat(actionStatusRepository.findAll()).haveAtLeast(1, new ActionStatusCondition(Status.WARNING)); + assertThat(actionStatusRepository.findAll()).haveAtLeast(1, new ActionStatusCondition(Status.CANCELED)); + + current = System.currentTimeMillis(); + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/" + action.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(action.getId().toString(), "closed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + myT = targetManagement.findTargetByControllerID("4712"); + assertThat(myT.getTargetInfo().getLastTargetQuery()).isGreaterThanOrEqualTo(current); + assertThat(myT.getTargetInfo().getUpdateStatus()).isEqualTo(TargetUpdateStatus.IN_SYNC); + assertThat(deploymentManagement.findActiveActionsByTarget(myT)).hasSize(0); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.ERROR)) + .hasSize(0); + assertThat(targetManagement.findTargetByUpdateStatus(new PageRequest(0, 10), TargetUpdateStatus.IN_SYNC)) + .hasSize(1); + + assertThat(actionStatusRepository.findAll()).hasSize(10); + assertThat(actionStatusRepository.findAll()).haveAtLeast(7, new ActionStatusCondition(Status.RUNNING)); + assertThat(actionStatusRepository.findAll()).haveAtLeast(1, new ActionStatusCondition(Status.WARNING)); + assertThat(actionStatusRepository.findAll()).haveAtLeast(1, new ActionStatusCondition(Status.CANCELED)); + assertThat(actionStatusRepository.findAll()).haveAtLeast(1, new ActionStatusCondition(Status.FINISHED)); + + assertThat(targetRepository.findByTargetInfoInstalledDistributionSet(new PageRequest(0, 10), ds)).hasSize(1); + assertThat(targetRepository.findByAssignedDistributionSet(new PageRequest(0, 10), ds)).hasSize(1); + assertThat(targetRepository + .findByAssignedDistributionSetOrTargetInfoInstalledDistributionSet(new PageRequest(0, 10), ds, ds)) + .hasSize(1); + } + + @Test + @Description("Various forbidden request appempts on the feedback resource. Ensures correct answering behaviour as expected to these kind of errors.") + public void badDeplomentActionFeedback() throws Exception { + final Target target = new Target("4712"); + final Target target2 = new Target("4713"); + final DistributionSet savedSet = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + final DistributionSet savedSet2 = TestDataUtil.generateDistributionSet("1", softwareManagement, + distributionSetManagement); + + // target does not exist + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/1234/feedback", tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionInProgressFeedback("1234")).contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + Target savedTarget = targetManagement.createTarget(target); + final Target savedTarget2 = targetManagement.createTarget(target2); + + // Action does not exists + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/1234/feedback", tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionInProgressFeedback("1234")).contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + final List toAssign = new ArrayList(); + toAssign.add(savedTarget); + final List toAssign2 = new ArrayList(); + toAssign2.add(savedTarget2); + + assertThat(targetManagement.findTargetByControllerID("4712").getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.UNKNOWN); + savedTarget = deploymentManagement.assignDistributionSet(savedSet, toAssign).getAssignedTargets().iterator() + .next(); + deploymentManagement.assignDistributionSet(savedSet2, toAssign2); + + // wrong format + mvc.perform(post("/{tenant}/controller/v1/4712/deploymentBase/AAAA/feedback", tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionInProgressFeedback("AAAA")).contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()); + + final Action updateAction = deploymentManagement.findActiveActionsByTarget(savedTarget).get(0); + + // action exists but is not assigned to this target + mvc.perform(post("/{tenant}/controller/v1/4713/deploymentBase/" + updateAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionInProgressFeedback(updateAction.getId().toString())) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + // not allowed methods + mvc.perform(get("/{tenant}/controller/v1/4712/deploymentBase/2/feedback", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + + mvc.perform(put("/{tenant}/controller/v1/4712/deploymentBase/2/feedback", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete("/{tenant}/controller/v1/4712/deploymentBase/2/feedback", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + + } + + private class ActionStatusCondition extends Condition { + private final Status status; + + /** + * @param status + */ + public ActionStatusCondition(final Status status) { + this.status = status; + } + + @Override + public boolean matches(final ActionStatus actionStatus) { + return actionStatus.getStatus() == status; + } + } +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/controller/RootControllerTest.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/controller/RootControllerTest.java new file mode 100755 index 000000000..1ad71a42c --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/controller/RootControllerTest.java @@ -0,0 +1,309 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.controller; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.startsWith; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; + +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.MockMvcResultPrinter; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.WithSpringAuthorityRule; +import org.eclipse.hawkbit.WithUser; +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.eclipse.hawkbit.rest.resource.JsonBuilder; +import org.eclipse.hawkbit.util.IpUtil; +import org.junit.Test; +import org.springframework.hateoas.MediaTypes; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +@ActiveProfiles({ "im", "test" }) +@Features("Component Tests - Controller RESTful API") +@Stories("Root Poll Resource") +// TODO: fully document tests -> @Description for long text and reasonable +// method name as short text +public class RootControllerTest extends AbstractIntegrationTest { + + @Test() + @Description("Ensures that software modules are not found, when target does not exists ") + public void testSoftwareModulesIfTargetNotExists() throws Exception { + final String targetNotExist = "targetNotExist"; + mvc.perform(get("/{tenant}/controller/v1/{targetNotExist}/softwaremodules/", tenantAware.getCurrentTenant(), + targetNotExist)).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + } + + @Test() + @Description("Ensures that software modules are not found, when assigned distribution set exists with no modules") + public void testSoftwareModulesEmptyIfDistributionSetNotExists() throws Exception { + mvc.perform(get("/{tenant}/controller/v1/{targetExist}/softwaremodules/", tenantAware.getCurrentTenant(), + TestDataUtil.createTarget(targetManagement).getName())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("$", hasSize(0))); + } + + @Test() + @Description("Ensures that software modules are found, when assigned distribution set exists with modules") + public void testAvailableSoftwareModulesWithNoArtifacts() throws Exception { + final Target target = TestDataUtil.createTarget(targetManagement); + final DistributionSet distributionSet = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + deploymentManagement.assignDistributionSet(distributionSet.getId(), new String[] { target.getName() }); + + final int modulesSize = distributionSet.getModules().size(); + + mvc.perform(get("/{tenant}/controller/v1/{targetExist}/softwaremodules/", tenantAware.getCurrentTenant(), + target.getName())).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(modulesSize))); + } + + @Test() + @Description("Ensures that targets cannot be created e.g. in plug'n play scenarios when tenant does not exists but can be created if the tenant exists.") + @WithUser(tenantId = "tenantDoesNotExists", allSpPermissions = true, authorities = "ROLE_CONTROLLER", autoCreateTenant = false) + public void targetCannotBeRegisteredIfTenantDoesNotExistsButWhenExists() throws Exception { + + mvc.perform(get("/default-tenant/", tenantAware.getCurrentTenant())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + // create tenant + systemManagement.getTenantMetadata("tenantDoesNotExists"); + + mvc.perform(get("/{}/controller/v1/aControllerId", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + // delete tenant again + systemManagement.deleteTenant("tenantDoesNotExists"); + + mvc.perform(get("/{}/controller/v1/aControllerId", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()); + + } + + @Test + @WithUser(principal = "knownPrincipal", authorities = { SpPermission.READ_TARGET, SpPermission.UPDATE_TARGET, + SpPermission.CREATE_TARGET }) + public void targetPollDoesNotModifyAuditData() throws Exception { + // create target first with "knownPrincipal" user and audit data + final String knownTargetControllerId = "target1"; + final String knownCreatedBy = "knownPrincipal"; + targetManagement.createTarget(new Target(knownTargetControllerId)); + final Target findTargetByControllerID = targetManagement.findTargetByControllerID(knownTargetControllerId); + assertThat(findTargetByControllerID.getCreatedBy()).isEqualTo(knownCreatedBy); + assertThat(findTargetByControllerID.getCreatedAt()).isNotNull(); + + // make a poll, audit information should not be changed, run as + // controller principal! + securityRule.runAs( + WithSpringAuthorityRule.withUser("controller", SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS), + new Callable() { + @Override + public Void call() throws Exception { + mvc.perform(get("/{tenant}/controller/v1/" + knownTargetControllerId, + tenantAware.getCurrentTenant())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + return null; + } + }); + + // verify that audit information has not changed + final Target targetVerify = targetManagement.findTargetByControllerID(knownTargetControllerId); + assertThat(targetVerify.getCreatedBy()).isEqualTo(findTargetByControllerID.getCreatedBy()); + assertThat(targetVerify.getCreatedAt()).isEqualTo(findTargetByControllerID.getCreatedAt()); + assertThat(targetVerify.getLastModifiedBy()).isEqualTo(findTargetByControllerID.getLastModifiedBy()); + assertThat(targetVerify.getLastModifiedAt()).isEqualTo(findTargetByControllerID.getLastModifiedAt()); + + } + + @Test + public void rootRsWithoutId() throws Exception { + mvc.perform(get("/controller/v1/")).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + } + + @Test + public void rootRsPlugAndPlay() throws Exception { + + final long current = System.currentTimeMillis(); + mvc.perform(get("/default-tenant/controller/v1/4711")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(content().contentType(MediaTypes.HAL_JSON)) + .andExpect(jsonPath("$config.polling.sleep", equalTo("00:01:00"))); + assertThat(targetManagement.findTargetByControllerID("4711").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + + assertThat(targetRepository.findByControllerId("4711").getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.REGISTERED); + + // not allowed methods + mvc.perform(post("/default-tenant/controller/v1/4711")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + mvc.perform(put("/default-tenant/controller/v1/4711")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete("/default-tenant/controller/v1/4711")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + } + + @Test + public void rootRsNotModified() throws Exception { + final String etag = mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaTypes.HAL_JSON)) + .andExpect(jsonPath("$config.polling.sleep", equalTo("00:01:00"))).andReturn().getResponse() + .getHeader("ETag"); + + mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant()).header("If-None-Match", etag)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotModified()); + + final Target target = targetRepository.findByControllerId("4711"); + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + deploymentManagement.assignDistributionSet(ds.getId(), new String[] { "4711" }); + + final Action updateAction = deploymentManagement.findActiveActionsByTarget(target).get(0); + final String etagWithFirstUpdate = mvc + .perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant()) + .header("If-None-Match", etag).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$_links.deploymentBase.href", + startsWith("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4711/deploymentBase/" + updateAction.getId()))) + .andReturn().getResponse().getHeader("ETag"); + + assertThat(etagWithFirstUpdate).isNotNull(); + + mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant()).header("If-None-Match", + etagWithFirstUpdate)).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotModified()); + + // now lets finish the update + mvc.perform(post("/{tenant}/controller/v1/4711/deploymentBase/" + updateAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(updateAction.getId().toString(), "closed")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + // we are again at the original state + mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant()).header("If-None-Match", etag)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotModified()); + + // Now another deployment + final DistributionSet ds2 = TestDataUtil.generateDistributionSet("2", softwareManagement, + distributionSetManagement); + + deploymentManagement.assignDistributionSet(ds2.getId(), new String[] { "4711" }); + + final Action updateAction2 = deploymentManagement.findActiveActionsByTarget(target).get(0); + + mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant()) + .header("If-None-Match", etagWithFirstUpdate).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$config.polling.sleep", equalTo("00:01:00"))) + .andExpect(jsonPath("$_links.deploymentBase.href", + startsWith("http://localhost/" + tenantAware.getCurrentTenant() + + "/controller/v1/4711/deploymentBase/" + updateAction2.getId()))) + .andReturn().getResponse().getHeader("ETag"); + } + + @Test + public void rootRsPrecommissioned() throws Exception { + final Target target = new Target("4711"); + targetManagement.createTarget(target); + + assertThat(targetRepository.findByControllerId("4711").getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.UNKNOWN); + + final long current = System.currentTimeMillis(); + mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaTypes.HAL_JSON)) + .andExpect(jsonPath("$config.polling.sleep", equalTo("00:01:00"))); + Thread.sleep(1); // is required: otherwise processing the next line is + // often too fast and + // the following assert will fail + assertThat(targetManagement.findTargetByControllerID("4711").getTargetInfo().getLastTargetQuery()) + .isLessThanOrEqualTo(System.currentTimeMillis()); + assertThat(targetManagement.findTargetByControllerID("4711").getTargetInfo().getLastTargetQuery()) + .isGreaterThanOrEqualTo(current); + + assertThat(targetRepository.findByControllerId("4711").getTargetInfo().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.REGISTERED); + } + + @Test + public void rootRsPlugAndPlayIpAddress() throws Exception { + // test + final String knownControllerId1 = "0815"; + mvc.perform(get("/{tenant}/controller/v1/{controllerId}", tenantAware.getCurrentTenant(), knownControllerId1)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + // verify + final Target target = targetManagement.findTargetByControllerID(knownControllerId1); + assertThat(target.getTargetInfo().getAddress()).isEqualTo(IpUtil.createHttpUri("127.0.0.1")); + + } + + @Test + @Description("Controller trys to finish an update process after it has been finished by an error action status.") + public void tryToFinishAnUpdateProcessAfterItHasBeenFinished() throws Exception { + + // mock + final Target target = new Target("911"); + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + Target savedTarget = targetManagement.createTarget(target); + final List toAssign = new ArrayList(); + toAssign.add(savedTarget); + savedTarget = deploymentManagement.assignDistributionSet(ds, toAssign).getAssignedTargets().iterator().next(); + final Action savedAction = deploymentManagement.findActiveActionsByTarget(savedTarget).get(0); + mvc.perform(post("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "/feedback", + tenantAware.getCurrentTenant()) + .content(JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "proceeding")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + mvc.perform(post("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "/feedback", + tenantAware.getCurrentTenant()).content( + JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "closed", "failure")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + mvc.perform(post("/{tenant}/controller/v1/911/deploymentBase/" + savedAction.getId() + "/feedback", + tenantAware.getCurrentTenant()).content( + JsonBuilder.deploymentActionFeedback(savedAction.getId().toString(), "closed", "success")) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isGone()); + } + +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/DistributionSetResourceTest.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/DistributionSetResourceTest.java new file mode 100755 index 000000000..72632afbf --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/DistributionSetResourceTest.java @@ -0,0 +1,916 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.fail; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.MockMvcResultPrinter; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.WithUser; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; +import org.eclipse.hawkbit.repository.model.DsMetadataCompositeKey; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.Target; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.context.annotation.Description; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; + +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +import com.google.common.collect.Lists; +import com.jayway.jsonpath.JsonPath; + +/** + * + * + * + */ +@Features("Component Tests - Management RESTful API") +@Stories("Distribution Set Resource") +// TODO: fully document tests -> @Description for long text and reasonable +// method name as short text +public class DistributionSetResourceTest extends AbstractIntegrationTest { + + @Test + @Description("This test verifies the call of all Software Modules that are assiged to a Distribution Set through the RESTful API.") + public void getSoftwaremodules() throws Exception { + // Create DistributionSet with three software modules + final DistributionSet set = TestDataUtil.generateDistributionSet("SMTest", softwareManagement, + distributionSetManagement); + mvc.perform(get(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + set.getId() + "/assignedSM")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.size", equalTo(set.getModules().size()))); + } + + @Test + @Description("This test verifies the deletion of a assigned Software Module of a Distribution Set can not be achieved when that Distribution Set has been assigned or installed to a target.") + public void testDeleteFailureWhenDistributionSetInUse() throws Exception { + + // create DisSet + final DistributionSet disSet = TestDataUtil.generateDistributionSetWithNoSoftwareModules("Eris", "560a", + distributionSetManagement); + final List smIDs = new ArrayList(); + SoftwareModule sm = new SoftwareModule(osType, "Dysnomia ", "15,772", null, null); + sm = softwareManagement.createSoftwareModule(sm); + smIDs.add(sm.getId()); + final JSONArray smList = new JSONArray(); + for (final Long smID : smIDs) { + smList.put(new JSONObject().put("id", Long.valueOf(smID))); + } + // post assignment + mvc.perform( + post(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + disSet.getId() + "/assignedSM") + .contentType(MediaType.APPLICATION_JSON).content(smList.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + // create targets and assign DisSet to target + final String[] knownTargetIds = new String[] { "1", "2" }; + final JSONArray list = new JSONArray(); + for (final String targetId : knownTargetIds) { + targetManagement.createTarget(new Target(targetId)); + list.put(new JSONObject().put("id", Long.valueOf(targetId))); + } + deploymentManagement.assignDistributionSet(disSet.getId(), knownTargetIds[0]); + mvc.perform( + post(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + disSet.getId() + "/assignedTargets") + .contentType(MediaType.APPLICATION_JSON).content(list.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.assigned", equalTo(knownTargetIds.length - 1))) + .andExpect(jsonPath("$.alreadyAssigned", equalTo(1))) + .andExpect(jsonPath("$.total", equalTo(knownTargetIds.length))); + + // try to delete the Software Module from DisSet that has been assigned + // to the target. + mvc.perform( + delete( + RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + disSet.getId() + "/assignedSM/" + + smIDs.get(0)).contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isLocked()) + .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.entitiylocked"))); + } + + @Test + @Description("This test verifies that the assignment of a Software Module to a Distribution Set can not be achieved when that Distribution Set has been assigned or installed to a target.") + public void testAssignmentFailureWhenAssigningToUsedDistributionSet() throws Exception { + + // create DisSet + final DistributionSet disSet = TestDataUtil.generateDistributionSetWithNoSoftwareModules("Mars", "686,980", + distributionSetManagement); + final List smIDs = new ArrayList(); + SoftwareModule sm = new SoftwareModule(osType, "Phobos", "0,3189", null, null); + sm = softwareManagement.createSoftwareModule(sm); + smIDs.add(sm.getId()); + final JSONArray smList = new JSONArray(); + for (final Long smID : smIDs) { + smList.put(new JSONObject().put("id", Long.valueOf(smID))); + } + // post assignment + mvc.perform( + post(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + disSet.getId() + "/assignedSM") + .contentType(MediaType.APPLICATION_JSON).content(smList.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + // create Targets + final String[] knownTargetIds = new String[] { "1", "2" }; + final JSONArray list = new JSONArray(); + for (final String targetId : knownTargetIds) { + targetManagement.createTarget(new Target(targetId)); + list.put(new JSONObject().put("id", Long.valueOf(targetId))); + } + // assign DisSet to target and test assignment + deploymentManagement.assignDistributionSet(disSet.getId(), knownTargetIds[0]); + mvc.perform( + post(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + disSet.getId() + "/assignedTargets") + .contentType(MediaType.APPLICATION_JSON).content(list.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.assigned", equalTo(knownTargetIds.length - 1))) + .andExpect(jsonPath("$.alreadyAssigned", equalTo(1))) + .andExpect(jsonPath("$.total", equalTo(knownTargetIds.length))); + + // Create another SM and post assignment + final List smID2s = new ArrayList(); + SoftwareModule sm2 = new SoftwareModule(appType, "Deimos", "1,262", null, null); + sm2 = softwareManagement.createSoftwareModule(sm2); + smID2s.add(sm2.getId()); + final JSONArray smList2 = new JSONArray(); + for (final Long smID : smID2s) { + smList2.put(new JSONObject().put("id", Long.valueOf(smID))); + } + + mvc.perform( + post(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + disSet.getId() + "/assignedSM") + .contentType(MediaType.APPLICATION_JSON).content(smList2.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isLocked()) + .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.entitiylocked"))); + } + + @Test + @Description("This test verifies the assignment of Software Modules to a Distribution Set through the RESTful API.") + public void assignSoftwaremoduleToDistributionSet() throws Exception { + + // create DisSet + final DistributionSet disSet = TestDataUtil.generateDistributionSetWithNoSoftwareModules("Jupiter", "398,88", + distributionSetManagement); + // Test if size is 0 + mvc.perform(get(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + disSet.getId() + "/assignedSM")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.size", equalTo(disSet.getModules().size()))); + // create Software Modules + final List smIDs = new ArrayList(); + SoftwareModule sm = new SoftwareModule(osType, "Europa", "3,551", null, null); + sm = softwareManagement.createSoftwareModule(sm); + smIDs.add(sm.getId()); + SoftwareModule sm2 = new SoftwareModule(appType, "Ganymed", "7,155", null, null); + sm2 = softwareManagement.createSoftwareModule(sm2); + smIDs.add(sm2.getId()); + SoftwareModule sm3 = new SoftwareModule(runtimeType, "Kallisto", "16,689", null, null); + sm3 = softwareManagement.createSoftwareModule(sm3); + smIDs.add(sm3.getId()); + final JSONArray list = new JSONArray(); + for (final Long smID : smIDs) { + list.put(new JSONObject().put("id", Long.valueOf(smID))); + } + // post assignment + mvc.perform( + post(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + disSet.getId() + "/assignedSM") + .contentType(MediaType.APPLICATION_JSON).content(list.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + // Test if size is 3 + mvc.perform(get(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + disSet.getId() + "/assignedSM")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.size", equalTo(smIDs.size()))); + } + + @Test + @Description("This test verifies the rejection of Software Modules of a Distribution Set through the RESTful API.") + public void unassignSoftwaremoduleFromDistributionSet() throws Exception { + + // Create DistributionSet with three software modules + final DistributionSet set = TestDataUtil.generateDistributionSet("Venus", softwareManagement, + distributionSetManagement); + int amountOfSM = set.getModules().size(); + mvc.perform(get(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + set.getId() + "/assignedSM")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.size", equalTo(amountOfSM))); + // test the rejection of all assigned software modules one by one + for (final Iterator iter = set.getModules().iterator(); iter.hasNext();) { + final Long smId = iter.next().getId(); + mvc.perform( + delete(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + set.getId() + "/assignedSM/" + smId)) + .andExpect(status().isOk()); + mvc.perform(get(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + set.getId() + "/assignedSM")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.size", equalTo(--amountOfSM))); + } + } + + @Test + public void assignMultipleTargetsToDistributionSet() throws Exception { + // prepare distribution set + final Set createDistributionSetsAlphabetical = createDistributionSetsAlphabetical(1); + final DistributionSet createdDs = createDistributionSetsAlphabetical.iterator().next(); + // prepare targets + final String[] knownTargetIds = new String[] { "1", "2", "3", "4", "5" }; + final JSONArray list = new JSONArray(); + for (final String targetId : knownTargetIds) { + targetManagement.createTarget(new Target(targetId)); + list.put(new JSONObject().put("id", Long.valueOf(targetId))); + } + // assign already one target to DS + deploymentManagement.assignDistributionSet(createdDs.getId(), knownTargetIds[0]); + + mvc.perform( + post(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + createdDs.getId() + "/assignedTargets") + .contentType(MediaType.APPLICATION_JSON).content(list.toString())).andExpect(status().isOk()) + .andExpect(jsonPath("$.assigned", equalTo(knownTargetIds.length - 1))) + .andExpect(jsonPath("$.alreadyAssigned", equalTo(1))) + .andExpect(jsonPath("$.total", equalTo(knownTargetIds.length))); + } + + @Test + public void getAssignedTargetsOfDistributionSet() throws Exception { + // prepare distribution set + final String knownTargetId = "knownTargetId1"; + final Set createDistributionSetsAlphabetical = createDistributionSetsAlphabetical(1); + final DistributionSet createdDs = createDistributionSetsAlphabetical.iterator().next(); + targetManagement.createTarget(new Target(knownTargetId)); + deploymentManagement.assignDistributionSet(createdDs.getId(), knownTargetId); + + mvc.perform( + get(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + createdDs.getId() + "/assignedTargets")) + .andExpect(status().isOk()).andExpect(jsonPath("$.size", equalTo(1))) + .andExpect(jsonPath("$.content[0].controllerId", equalTo(knownTargetId))); + } + + @Test + public void getAssignedTargetsOfDistributionSetIsEmpty() throws Exception { + final Set createDistributionSetsAlphabetical = createDistributionSetsAlphabetical(1); + final DistributionSet createdDs = createDistributionSetsAlphabetical.iterator().next(); + mvc.perform( + get(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + createdDs.getId() + "/assignedTargets")) + .andExpect(status().isOk()).andExpect(jsonPath("$.size", equalTo(0))) + .andExpect(jsonPath("$.total", equalTo(0))); + } + + @Test + public void getInstalledTargetsOfDistributionSet() throws Exception { + // prepare distribution set + final String knownTargetId = "knownTargetId1"; + final Set createDistributionSetsAlphabetical = createDistributionSetsAlphabetical(1); + final DistributionSet createdDs = createDistributionSetsAlphabetical.iterator().next(); + final Target createTarget = targetManagement.createTarget(new Target(knownTargetId)); + // create some dummy targets which are not assigned or installed + targetManagement.createTarget(new Target("dummy1")); + targetManagement.createTarget(new Target("dummy2")); + // assign knownTargetId to distribution set + deploymentManagement.assignDistributionSet(createdDs.getId(), knownTargetId); + // make it in install state + sendUpdateActionStatusToTargets(createdDs, Lists.newArrayList(createTarget), Status.FINISHED, "some message"); + + mvc.perform( + get(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + createdDs.getId() + "/installedTargets")) + .andExpect(status().isOk()).andExpect(jsonPath("$.size", equalTo(1))) + .andExpect(jsonPath("$.content[0].controllerId", equalTo(knownTargetId))); + } + + @Test + public void testGetDistributionSetsWithoutAddtionalRequestParameters() throws Exception { + final int modules = 5; + createDistributionSetsAlphabetical(modules); + mvc.perform(get(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_TOTAL, equalTo(modules))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_SIZE, equalTo(modules))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_CONTENT, hasSize(modules))); + } + + @Test + public void testGetDistributionSetsWithPagingLimitRequestParameter() throws Exception { + final int modules = 5; + final int limitSize = 1; + createDistributionSetsAlphabetical(modules); + mvc.perform( + get(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING).param( + RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(limitSize))) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_TOTAL, equalTo(modules))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_SIZE, equalTo(limitSize))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_CONTENT, hasSize(limitSize))); + } + + @Test + public void testGetDistributionSetsWithPagingLimitAndOffsetRequestParameter() throws Exception { + final int modules = 5; + final int offsetParam = 2; + final int expectedSize = modules - offsetParam; + createDistributionSetsAlphabetical(modules); + mvc.perform( + get(RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING).param( + RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, String.valueOf(offsetParam)).param( + RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(modules))) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_TOTAL, equalTo(modules))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_SIZE, equalTo(expectedSize))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_CONTENT, hasSize(expectedSize))); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + public void testGetDistributionSets() throws Exception { + // prepare test data + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, false, true)).hasSize(0); + + DistributionSet set = TestDataUtil + .generateDistributionSet("one", softwareManagement, distributionSetManagement); + set.setRequiredMigrationStep(set.isRequiredMigrationStep()); + set = distributionSetManagement.updateDistributionSet(set); + + set.setVersion("anotherVersion"); + set = distributionSetManagement.updateDistributionSet(set); + + // load also lazy stuff + set = distributionSetManagement.findDistributionSetByIdWithDetails(set.getId()); + + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, false, true)).hasSize(1); + + // perform request + mvc.perform(get("/rest/v1/distributionsets").accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + jsonPath("$content.[0]._links.self.href", equalTo("http://localhost/rest/v1/distributionsets/" + + set.getId()))) + .andExpect(jsonPath("$content.[0].id", equalTo(set.getId().intValue()))) + .andExpect(jsonPath("$content.[0].name", equalTo(set.getName()))) + .andExpect(jsonPath("$content.[0].requiredMigrationStep", equalTo(set.isRequiredMigrationStep()))) + .andExpect(jsonPath("$content.[0].description", equalTo(set.getDescription()))) + .andExpect(jsonPath("$content.[0].type", equalTo(set.getType().getKey()))) + .andExpect(jsonPath("$content.[0].createdBy", equalTo(set.getCreatedBy()))) + .andExpect(jsonPath("$content.[0].createdAt", equalTo(set.getCreatedAt()))) + .andExpect(jsonPath("$content.[0].complete", equalTo(Boolean.TRUE))) + .andExpect(jsonPath("$content.[0].lastModifiedBy", equalTo(set.getLastModifiedBy()))) + .andExpect(jsonPath("$content.[0].lastModifiedAt", equalTo(set.getLastModifiedAt()))) + .andExpect(jsonPath("$content.[0].version", equalTo(set.getVersion()))) + .andExpect( + jsonPath("$content.[0].modules.[?(@.type==" + runtimeType.getKey() + ")][0].id", equalTo(set + .findFirstModuleByType(runtimeType).getId().intValue()))) + .andExpect( + jsonPath("$content.[0].modules.[?(@.type==" + appType.getKey() + ")][0].id", equalTo(set + .findFirstModuleByType(appType).getId().intValue()))) + .andExpect( + jsonPath("$content.[0].modules.[?(@.type==" + osType.getKey() + ")][0].id", equalTo(set + .findFirstModuleByType(osType).getId().intValue()))); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + public void testGetDistributionSet() throws Exception { + // prepare test data + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, false, true)).hasSize(0); + + DistributionSet set = TestDataUtil + .generateDistributionSet("one", softwareManagement, distributionSetManagement); + set.setVersion("anotherVersion"); + set = distributionSetManagement.updateDistributionSet(set); + + // load also lazy stuff + set = distributionSetManagement.findDistributionSetByIdWithDetails(set.getId()); + + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, false, true)).hasSize(1); + + // perform request + mvc.perform(get("/rest/v1/distributionsets/{dsId}", set.getId()).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + jsonPath("$_links.self.href", + equalTo("http://localhost/rest/v1/distributionsets/" + set.getId()))) + .andExpect(jsonPath("$id", equalTo(set.getId().intValue()))) + .andExpect(jsonPath("$name", equalTo(set.getName()))) + .andExpect(jsonPath("$type", equalTo(set.getType().getKey()))) + .andExpect(jsonPath("$description", equalTo(set.getDescription()))) + .andExpect(jsonPath("$requiredMigrationStep", equalTo(set.isRequiredMigrationStep()))) + .andExpect(jsonPath("$createdBy", equalTo(set.getCreatedBy()))) + .andExpect(jsonPath("$complete", equalTo(Boolean.TRUE))) + .andExpect(jsonPath("$createdAt", equalTo(set.getCreatedAt()))) + .andExpect(jsonPath("$lastModifiedBy", equalTo(set.getLastModifiedBy()))) + .andExpect(jsonPath("$lastModifiedAt", equalTo(set.getLastModifiedAt()))) + .andExpect(jsonPath("$version", equalTo(set.getVersion()))) + .andExpect( + jsonPath("$modules.[?(@.type==" + runtimeType.getKey() + ")][0].id", equalTo(set + .findFirstModuleByType(runtimeType).getId().intValue()))) + .andExpect( + jsonPath("$modules.[?(@.type==" + appType.getKey() + ")][0].id", equalTo(set + .findFirstModuleByType(appType).getId().intValue()))) + .andExpect( + jsonPath("$modules.[?(@.type==" + osType.getKey() + ")][0].id", equalTo(set + .findFirstModuleByType(osType).getId().intValue()))); + + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + public void testCreateDistributionSets() throws JSONException, Exception { + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, false, true)).hasSize(0); + + final SoftwareModule ah = softwareManagement.createSoftwareModule(new SoftwareModule(appType, "agent-hub", + "1.0.1", null, "")); + final SoftwareModule jvm = softwareManagement.createSoftwareModule(new SoftwareModule(runtimeType, + "oracle-jre", "1.7.2", null, "")); + final SoftwareModule os = softwareManagement.createSoftwareModule(new SoftwareModule(osType, "poky", "3.0.2", + null, "")); + + DistributionSet one = TestDataUtil.buildDistributionSet("one", "one", standardDsType, os, jvm, ah); + DistributionSet two = TestDataUtil.buildDistributionSet("two", "two", standardDsType, os, jvm, ah); + DistributionSet three = TestDataUtil.buildDistributionSet("three", "three", standardDsType, os, jvm, ah); + three.setRequiredMigrationStep(true); + + final List sets = new ArrayList<>(); + sets.add(one); + sets.add(two); + sets.add(three); + + final long current = System.currentTimeMillis(); + + final MvcResult mvcResult = mvc + .perform( + post("/rest/v1/distributionsets/").content(JsonBuilder.distributionSets(sets)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()) + .andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("[0]name", equalTo(one.getName()))) + .andExpect(jsonPath("[0]description", equalTo(one.getDescription()))) + .andExpect(jsonPath("[0]type", equalTo(standardDsType.getKey()))) + .andExpect(jsonPath("[0]createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("[0]version", equalTo(one.getVersion()))) + .andExpect(jsonPath("[0]complete", equalTo(Boolean.TRUE))) + .andExpect(jsonPath("[0]requiredMigrationStep", equalTo(one.isRequiredMigrationStep()))) + .andExpect( + jsonPath("[0].modules.[?(@.type==" + runtimeType.getKey() + ")][0].id", equalTo(one + .findFirstModuleByType(runtimeType).getId().intValue()))) + .andExpect( + jsonPath("[0].modules.[?(@.type==" + appType.getKey() + ")][0].id", equalTo(one + .findFirstModuleByType(appType).getId().intValue()))) + .andExpect( + jsonPath("[0].modules.[?(@.type==" + osType.getKey() + ")][0].id", equalTo(one + .findFirstModuleByType(osType).getId().intValue()))) + .andExpect(jsonPath("[1]name", equalTo(two.getName()))) + .andExpect(jsonPath("[1]description", equalTo(two.getDescription()))) + .andExpect(jsonPath("[1]complete", equalTo(Boolean.TRUE))) + .andExpect(jsonPath("[1]type", equalTo(standardDsType.getKey()))) + .andExpect(jsonPath("[1]createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("[1]version", equalTo(two.getVersion()))) + .andExpect( + jsonPath("[1].modules.[?(@.type==" + runtimeType.getKey() + ")][0].id", equalTo(two + .findFirstModuleByType(runtimeType).getId().intValue()))) + .andExpect( + jsonPath("[1].modules.[?(@.type==" + appType.getKey() + ")][0].id", equalTo(two + .findFirstModuleByType(appType).getId().intValue()))) + .andExpect( + jsonPath("[1].modules.[?(@.type==" + osType.getKey() + ")][0].id", equalTo(two + .findFirstModuleByType(osType).getId().intValue()))) + .andExpect(jsonPath("[1]requiredMigrationStep", equalTo(two.isRequiredMigrationStep()))) + .andExpect(jsonPath("[2]name", equalTo(three.getName()))) + .andExpect(jsonPath("[2]description", equalTo(three.getDescription()))) + .andExpect(jsonPath("[2]complete", equalTo(Boolean.TRUE))) + .andExpect(jsonPath("[2]type", equalTo(standardDsType.getKey()))) + .andExpect(jsonPath("[2]createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("[2]version", equalTo(three.getVersion()))) + .andExpect( + jsonPath("[2].modules.[?(@.type==" + runtimeType.getKey() + ")][0].id", equalTo(three + .findFirstModuleByType(runtimeType).getId().intValue()))) + .andExpect( + jsonPath("[2].modules.[?(@.type==" + appType.getKey() + ")][0].id", equalTo(three + .findFirstModuleByType(appType).getId().intValue()))) + .andExpect( + jsonPath("[2].modules.[?(@.type==" + osType.getKey() + ")][0].id", equalTo(three + .findFirstModuleByType(osType).getId().intValue()))) + .andExpect(jsonPath("[2]requiredMigrationStep", equalTo(three.isRequiredMigrationStep()))).andReturn(); + + one = distributionSetManagement.findDistributionSetByIdWithDetails(distributionSetManagement + .findDistributionSetByNameAndVersion("one", "one").getId()); + two = distributionSetManagement.findDistributionSetByIdWithDetails(distributionSetManagement + .findDistributionSetByNameAndVersion("two", "two").getId()); + three = distributionSetManagement.findDistributionSetByIdWithDetails(distributionSetManagement + .findDistributionSetByNameAndVersion("three", "three").getId()); + + assertThat( + JsonPath.compile("[0]_links.self.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/distributionsets/" + one.getId()); + assertThat( + JsonPath.compile("[0]_links.type.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/distributionsettypes/" + one.getType().getId()); + + assertThat(JsonPath.compile("[0]id").read(mvcResult.getResponse().getContentAsString()).toString()).isEqualTo( + String.valueOf(one.getId())); + assertThat( + JsonPath.compile("[1]_links.self.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/distributionsets/" + two.getId()); + assertThat( + JsonPath.compile("[1]_links.type.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/distributionsettypes/" + two.getType().getId()); + + assertThat(JsonPath.compile("[1]id").read(mvcResult.getResponse().getContentAsString()).toString()).isEqualTo( + String.valueOf(two.getId())); + assertThat( + JsonPath.compile("[2]_links.self.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/distributionsets/" + three.getId()); + assertThat( + JsonPath.compile("[2]_links.type.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/distributionsettypes/" + three.getType().getId()); + + assertThat(JsonPath.compile("[2]id").read(mvcResult.getResponse().getContentAsString()).toString()).isEqualTo( + String.valueOf(three.getId())); + + // check in database + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, false, true)).hasSize(3); + assertThat(one.isRequiredMigrationStep()).isEqualTo(false); + assertThat(two.isRequiredMigrationStep()).isEqualTo(false); + assertThat(three.isRequiredMigrationStep()).isEqualTo(true); + + assertThat(one.getCreatedAt()).isGreaterThanOrEqualTo(current); + assertThat(two.getCreatedAt()).isGreaterThanOrEqualTo(current); + assertThat(three.getCreatedAt()).isGreaterThanOrEqualTo(current); + } + + @Test + public void testDeleteUnassignedistributionSet() throws Exception { + // prepare test data + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, false, true)).hasSize(0); + + final DistributionSet set = TestDataUtil.generateDistributionSet("one", softwareManagement, + distributionSetManagement); + + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, false, true)).hasSize(1); + + // perform request + mvc.perform(delete("/rest/v1/distributionsets/{smId}", set.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + // check repository content + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, false, true)).isEmpty(); + assertThat(distributionSetRepository.findAll()).isEmpty(); + } + + @Test + public void testDeleteAssignedDistributionSet() throws Exception { + // prepare test data + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, false, true)).hasSize(0); + + final DistributionSet set = TestDataUtil.generateDistributionSet("one", softwareManagement, + distributionSetManagement); + targetManagement.createTarget(new Target("test")); + deploymentManagement.assignDistributionSet(set.getId(), "test"); + + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, false, true)).hasSize(1); + + // perform request + mvc.perform(delete("/rest/v1/distributionsets/{smId}", set.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + // check repository content + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, false, true)).hasSize(0); + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, true, true)).hasSize(1); + } + + @Test + public void testUpdateDistributionSet() throws Exception { + + // prepare test data + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, false, true)).hasSize(0); + + final DistributionSet set = TestDataUtil.generateDistributionSet("one", softwareManagement, + distributionSetManagement); + + assertThat(distributionSetManagement.findDistributionSetsAll(pageReq, false, true)).hasSize(1); + + final DistributionSet update = new DistributionSet(); + update.setVersion("anotherVersion"); + update.setName(null); + update.setType(standardDsType); + + mvc.perform( + put("/rest/v1/distributionsets/{dsId}", set.getId()).content(JsonBuilder.distributionSet(update)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + assertThat( + distributionSetManagement.findDistributionSetsAll(pageReq, false, true).getContent().get(0) + .getVersion()).isEqualTo("anotherVersion"); + assertThat( + distributionSetManagement.findDistributionSetsAll(pageReq, false, true).getContent().get(0).getName()) + .isEqualTo(set.getName()); + } + + @Test + public void testInvalidRequestsOnDistributionSetsResource() throws Exception { + final DistributionSet set = TestDataUtil.generateDistributionSet("one", softwareManagement, + distributionSetManagement); + + final List sets = new ArrayList<>(); + sets.add(set); + + // SM does not exist + mvc.perform(get("/rest/v1/distributionsets/12345678")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + mvc.perform(delete("/rest/v1/distributionsets/12345678")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + // bad request - no content + mvc.perform(post("/rest/v1/distributionsets").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()); + + // bad request - bad content + mvc.perform( + post("/rest/v1/distributionsets").content("sdfjsdlkjfskdjf".getBytes()).contentType( + MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()); + + // unsupported media type + mvc.perform( + post("/rest/v1/distributionsets").content(JsonBuilder.distributionSets(sets)).contentType( + MediaType.APPLICATION_OCTET_STREAM)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isUnsupportedMediaType()); + + // not allowed methods + mvc.perform(post("/rest/v1/distributionsets/{smId}", set.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + mvc.perform(put("/rest/v1/distributionsets")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete("/rest/v1/distributionsets")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + } + + @Test + public void createMetadata() throws Exception { + final DistributionSet testDS = TestDataUtil.generateDistributionSet("one", softwareManagement, + distributionSetManagement); + + final String knownKey1 = "knownKey1"; + final String knownKey2 = "knownKey2"; + + final String knownValue1 = "knownValue1"; + final String knownValue2 = "knownValue2"; + + final JSONArray jsonArray = new JSONArray(); + jsonArray.put(new JSONObject().put("key", knownKey1).put("value", knownValue1)); + jsonArray.put(new JSONObject().put("key", knownKey2).put("value", knownValue2)); + + mvc.perform( + post("/rest/v1/distributionsets/{dsId}/metadata", testDS.getId()).contentType( + MediaType.APPLICATION_JSON).content(jsonArray.toString())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isCreated()).andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("[0]key", equalTo(knownKey1))) + .andExpect(jsonPath("[0]value", equalTo(knownValue1))) + .andExpect(jsonPath("[1]key", equalTo(knownKey2))) + .andExpect(jsonPath("[1]value", equalTo(knownValue2))); + + final DistributionSetMetadata metaKey1 = distributionSetManagement.findOne(new DsMetadataCompositeKey(testDS, + knownKey1)); + final DistributionSetMetadata metaKey2 = distributionSetManagement.findOne(new DsMetadataCompositeKey(testDS, + knownKey2)); + + assertThat(metaKey1.getValue()).isEqualTo(knownValue1); + assertThat(metaKey2.getValue()).isEqualTo(knownValue2); + } + + @Test + public void updateMetadata() throws Exception { + // prepare and create metadata for update + final String knownKey = "knownKey"; + final String knownValue = "knownValue"; + final String updateValue = "valueForUpdate"; + + final DistributionSet testDS = TestDataUtil.generateDistributionSet("one", softwareManagement, + distributionSetManagement); + distributionSetManagement.createDistributionSetMetadata(new DistributionSetMetadata(knownKey, testDS, + knownValue)); + + final JSONObject jsonObject = new JSONObject().put("key", knownKey).put("value", updateValue); + + mvc.perform( + put("/rest/v1/distributionsets/{dsId}/metadata/{key}", testDS.getId(), knownKey).contentType( + MediaType.APPLICATION_JSON).content(jsonObject.toString())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("key", equalTo(knownKey))).andExpect(jsonPath("value", equalTo(updateValue))); + + final DistributionSetMetadata assertDS = distributionSetManagement.findOne(new DsMetadataCompositeKey(testDS, + knownKey)); + assertThat(assertDS.getValue()).isEqualTo(updateValue); + + } + + @Test + public void deleteMetadata() throws Exception { + // prepare and create metadata for deletion + final String knownKey = "knownKey"; + final String knownValue = "knownValue"; + + final DistributionSet testDS = TestDataUtil.generateDistributionSet("one", softwareManagement, + distributionSetManagement); + distributionSetManagement.createDistributionSetMetadata(new DistributionSetMetadata(knownKey, testDS, + knownValue)); + + mvc.perform(delete("/rest/v1/distributionsets/{dsId}/metadata/{key}", testDS.getId(), knownKey)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + try { + distributionSetManagement.findOne(new DsMetadataCompositeKey(testDS, knownKey)); + fail("expected EntityNotFoundException but didn't throw"); + } catch (final EntityNotFoundException e) { + // ok as expected + } + } + + @Test + public void getSingleMetadata() throws Exception { + // prepare and create metadata + final String knownKey = "knownKey"; + final String knownValue = "knownValue"; + final DistributionSet testDS = TestDataUtil.generateDistributionSet("one", softwareManagement, + distributionSetManagement); + distributionSetManagement.createDistributionSetMetadata(new DistributionSetMetadata(knownKey, testDS, + knownValue)); + + mvc.perform(get("/rest/v1/distributionsets/{dsId}/metadata/{key}", testDS.getId(), knownKey)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("key", equalTo(knownKey))).andExpect(jsonPath("value", equalTo(knownValue))); + } + + @Test + public void getPagedListofMetadata() throws Exception { + + final int totalMetadata = 10; + final int limitParam = 5; + final String offsetParam = "0"; + final String knownKeyPrefix = "knownKey"; + final String knownValuePrefix = "knownValue"; + final DistributionSet testDS = TestDataUtil.generateDistributionSet("one", softwareManagement, + distributionSetManagement); + for (int index = 0; index < totalMetadata; index++) { + distributionSetManagement.createDistributionSetMetadata(new DistributionSetMetadata(knownKeyPrefix + index, + distributionSetManagement.findDistributionSetById(testDS.getId()), knownValuePrefix + index)); + } + + mvc.perform( + get("/rest/v1/distributionsets/{dsId}/metadata?offset=" + offsetParam + "&limit=" + limitParam, + testDS.getId())).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("size", equalTo(limitParam))).andExpect(jsonPath("total", equalTo(totalMetadata))); + } + + @Test + public void searchDistributionSetRsql() throws Exception { + final String dsSuffix = "test"; + final int amount = 10; + TestDataUtil.generateDistributionSets(dsSuffix, amount, softwareManagement, distributionSetManagement); + TestDataUtil.generateDistributionSet("DS1test", softwareManagement, distributionSetManagement); + TestDataUtil.generateDistributionSet("DS2test", softwareManagement, distributionSetManagement); + + final String rsqlFindLikeDs1OrDs2 = "name==DS1test,name==DS2test"; + + mvc.perform(get("/rest/v1/distributionsets?q=" + rsqlFindLikeDs1OrDs2)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("size", equalTo(2))) + .andExpect(jsonPath("total", equalTo(2))).andExpect(jsonPath("content[0].name", equalTo("DS1test"))) + .andExpect(jsonPath("content[1].name", equalTo("DS2test"))); + + } + + @Ignore + @Test + public void filterDistributionSetComplete() throws Exception { + final int amount = 10; + TestDataUtil.generateDistributionSets(amount, softwareManagement, distributionSetManagement); + + final String rsqlFindLikeDs1OrDs2 = "complete==" + Boolean.TRUE; + + mvc.perform(get("/rest/v1/distributionsets?q=" + rsqlFindLikeDs1OrDs2)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("size", equalTo(10))) + .andExpect(jsonPath("total", equalTo(10))); + } + + @Test + public void searchDistributionSetAssignedTargetsRsql() throws Exception { + // prepare distribution set + final Set createDistributionSetsAlphabetical = createDistributionSetsAlphabetical(1); + final DistributionSet createdDs = createDistributionSetsAlphabetical.iterator().next(); + // prepare targets + final String[] knownTargetIds = new String[] { "1", "2", "3", "4", "5" }; + final JSONArray list = new JSONArray(); + for (final String targetId : knownTargetIds) { + targetManagement.createTarget(new Target(targetId)); + list.put(new JSONObject().put("id", Long.valueOf(targetId))); + } + + // assign already one target to DS + deploymentManagement.assignDistributionSet(createdDs.getId(), knownTargetIds[0]); + + final String rsqlFindTargetId1 = "controllerId==1"; + + mvc.perform( + get( + RestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + createdDs.getId() + + "/assignedTargets?q=" + rsqlFindTargetId1).contentType(MediaType.APPLICATION_JSON) + .content(list.toString())).andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(1))) + .andExpect(jsonPath("size", equalTo(1))).andExpect(jsonPath("content[0].controllerId", equalTo("1"))); + } + + @Test + public void searchDistributionSetMetadataRsql() throws Exception { + final int totalMetadata = 10; + final String knownKeyPrefix = "knownKey"; + final String knownValuePrefix = "knownValue"; + final DistributionSet testDS = TestDataUtil.generateDistributionSet("one", softwareManagement, + distributionSetManagement); + for (int index = 0; index < totalMetadata; index++) { + distributionSetManagement.createDistributionSetMetadata(new DistributionSetMetadata(knownKeyPrefix + index, + distributionSetManagement.findDistributionSetById(testDS.getId()), knownValuePrefix + index)); + } + + final String rsqlSearchValue1 = "value==knownValue1"; + + mvc.perform(get("/rest/v1/distributionsets/{dsId}/metadata?q=" + rsqlSearchValue1, testDS.getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("size", equalTo(1))) + .andExpect(jsonPath("total", equalTo(1))).andExpect(jsonPath("content[0].key", equalTo("knownKey1"))) + .andExpect(jsonPath("content[0].value", equalTo("knownValue1"))); + } + + private Set createDistributionSetsAlphabetical(final int amount) { + char character = 'a'; + final Set created = new HashSet<>(); + for (int index = 0; index < amount; index++) { + final String str = String.valueOf(character); + created.add(TestDataUtil.generateDistributionSet(str, softwareManagement, distributionSetManagement)); + character++; + } + return created; + } + + private List sendUpdateActionStatusToTargets(final DistributionSet dsA, final Iterable targs, + final Status status, final String... msgs) { + final List result = new ArrayList(); + for (final Target t : targs) { + final List findByTarget = actionRepository.findByTarget(t); + for (final Action action : findByTarget) { + result.add(sendUpdateActionStatusToTarget(status, action, t, msgs)); + } + } + return result; + } + + private Target sendUpdateActionStatusToTarget(final Status status, final Action updActA, final Target t, + final String... msgs) { + updActA.setStatus(status); + + final ActionStatus statusMessages = new ActionStatus(); + statusMessages.setAction(updActA); + statusMessages.setOccurredAt(System.currentTimeMillis()); + statusMessages.setStatus(status); + for (final String msg : msgs) { + statusMessages.addMessage(msg); + } + controllerManagament.addUpdateActionStatus(statusMessages, updActA); + return targetManagement.findTargetByControllerID(t.getControllerId()); + } + +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/DistributionSetTypeResourceTest.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/DistributionSetTypeResourceTest.java new file mode 100755 index 000000000..266b284ca --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/DistributionSetTypeResourceTest.java @@ -0,0 +1,551 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.hasSize; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.MockMvcResultPrinter; +import org.eclipse.hawkbit.WithUser; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; + +import com.google.common.collect.Lists; +import com.jayway.jsonpath.JsonPath; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * Test for {@link DistributionSetTypeResource}. + * + * + * + * + */ +@Features("Component Tests - Management RESTful API") +@Stories("Distribution Set Type Resource") +public class DistributionSetTypeResourceTest extends AbstractIntegrationTest { + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/distributionsettypes GET requests.") + public void getDistributionSetTypes() throws Exception { + + DistributionSetType testType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("test123", "TestName123", "Desc123")); + testType.setDescription("Desc1234"); + testType = distributionSetManagement.updateDistributionSetType(testType); + + mvc.perform(get("/rest/v1/distributionsettypes").accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$content.[?(@.key==" + standardDsType.getKey() + ")][0].name", + equalTo(standardDsType.getName()))) + .andExpect(jsonPath("$content.[?(@.key==" + standardDsType.getKey() + ")][0].description", + equalTo(standardDsType.getDescription()))) + .andExpect(jsonPath("$content.[?(@.key==" + standardDsType.getKey() + ")][0].key", + equalTo(standardDsType.getKey()))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0].id", equalTo(testType.getId().intValue()))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0].name", equalTo("TestName123"))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0].description", equalTo("Desc1234"))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0].createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0].createdAt", equalTo(testType.getCreatedAt()))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0].lastModifiedBy", equalTo("uploadTester"))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0].lastModifiedAt", + equalTo(testType.getLastModifiedAt()))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0].key", equalTo("test123"))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0]._links.self.href", + equalTo("http://localhost/rest/v1/distributionsettypes/" + testType.getId()))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0]._links.mandatorymodules.href", + equalTo("http://localhost/rest/v1/distributionsettypes/" + testType.getId() + + "/mandatorymoduletypes"))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0]._links.optionalmodules.href", equalTo( + "http://localhost/rest/v1/distributionsettypes/" + testType.getId() + "/optionalmoduletypes"))) + .andExpect(jsonPath("$total", equalTo(4))); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/distributionsettypes POST requests.") + public void createDistributionSetTypes() throws JSONException, Exception { + + assertThat(distributionSetManagement.countDistributionSetTypesAll()).isEqualTo(3); + + final List types = new ArrayList<>(); + types.add(new DistributionSetType("test1", "TestName1", "Desc1").addMandatoryModuleType(osType) + .addOptionalModuleType(runtimeType)); + types.add(new DistributionSetType("test2", "TestName2", "Desc2").addOptionalModuleType(osType) + .addOptionalModuleType(runtimeType).addOptionalModuleType(appType)); + types.add(new DistributionSetType("test3", "TestName3", "Desc3").addMandatoryModuleType(osType) + .addMandatoryModuleType(runtimeType)); + + final MvcResult mvcResult = mvc + .perform(post("/rest/v1/distributionsettypes/").content(JsonBuilder.distributionSetTypes(types)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("[0].name", equalTo("TestName1"))).andExpect(jsonPath("[0].key", equalTo("test1"))) + .andExpect(jsonPath("[0].description", equalTo("Desc1"))) + .andExpect(jsonPath("[0].createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("[1].name", equalTo("TestName2"))).andExpect(jsonPath("[1].key", equalTo("test2"))) + .andExpect(jsonPath("[1].description", equalTo("Desc2"))) + .andExpect(jsonPath("[1].createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("[2].name", equalTo("TestName3"))).andExpect(jsonPath("[2].key", equalTo("test3"))) + .andExpect(jsonPath("[2].description", equalTo("Desc3"))) + .andExpect(jsonPath("[2].createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("[2].createdAt", not(equalTo(0)))).andReturn(); + + final DistributionSetType created1 = distributionSetManagement.findDistributionSetTypeByKey("test1"); + final DistributionSetType created2 = distributionSetManagement.findDistributionSetTypeByKey("test2"); + final DistributionSetType created3 = distributionSetManagement.findDistributionSetTypeByKey("test3"); + + assertThat(created1.getMandatoryModuleTypes()).containsOnly(osType); + assertThat(created1.getOptionalModuleTypes()).containsOnly(runtimeType); + assertThat(created2.getOptionalModuleTypes()).containsOnly(osType, runtimeType, appType); + assertThat(created3.getMandatoryModuleTypes()).containsOnly(osType, runtimeType); + + assertThat( + JsonPath.compile("[0]_links.self.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/distributionsettypes/" + created1.getId()); + assertThat( + JsonPath.compile("[1]_links.self.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/distributionsettypes/" + created2.getId()); + assertThat( + JsonPath.compile("[2]_links.self.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/distributionsettypes/" + created3.getId()); + + assertThat(JsonPath.compile("[0]_links.mandatorymodules.href") + .read(mvcResult.getResponse().getContentAsString()).toString()).isEqualTo( + "http://localhost/rest/v1/distributionsettypes/" + created1.getId() + "/mandatorymoduletypes"); + assertThat(JsonPath.compile("[1]_links.mandatorymodules.href") + .read(mvcResult.getResponse().getContentAsString()).toString()).isEqualTo( + "http://localhost/rest/v1/distributionsettypes/" + created2.getId() + "/mandatorymoduletypes"); + assertThat(JsonPath.compile("[2]_links.mandatorymodules.href") + .read(mvcResult.getResponse().getContentAsString()).toString()).isEqualTo( + "http://localhost/rest/v1/distributionsettypes/" + created3.getId() + "/mandatorymoduletypes"); + + assertThat(JsonPath.compile("[0]_links.optionalmodules.href").read(mvcResult.getResponse().getContentAsString()) + .toString()).isEqualTo( + "http://localhost/rest/v1/distributionsettypes/" + created1.getId() + "/optionalmoduletypes"); + assertThat(JsonPath.compile("[1]_links.optionalmodules.href").read(mvcResult.getResponse().getContentAsString()) + .toString()).isEqualTo( + "http://localhost/rest/v1/distributionsettypes/" + created2.getId() + "/optionalmoduletypes"); + assertThat(JsonPath.compile("[2]_links.optionalmodules.href").read(mvcResult.getResponse().getContentAsString()) + .toString()).isEqualTo( + "http://localhost/rest/v1/distributionsettypes/" + created3.getId() + "/optionalmoduletypes"); + + assertThat(distributionSetManagement.countDistributionSetTypesAll()).isEqualTo(6); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/distributionsettypes/{ID}/mandatorymoduletypes POST requests.") + public void addMandatoryModuleToDistributionSetType() throws JSONException, Exception { + DistributionSetType testType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("test123", "TestName123", "Desc123")); + assertThat(testType.getOptLockRevision()).isEqualTo(1); + + mvc.perform(post("/rest/v1/distributionsettypes/{dstID}/mandatorymoduletypes", testType.getId()) + .content("{\"id\":" + osType.getId() + "}").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + testType = distributionSetManagement.findDistributionSetTypeById(testType.getId()); + assertThat(testType.getLastModifiedBy()).isEqualTo("uploadTester"); + assertThat(testType.getOptLockRevision()).isEqualTo(2); + assertThat(testType.getMandatoryModuleTypes()).containsExactly(osType); + assertThat(testType.getOptionalModuleTypes()).isEmpty(); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/distributionsettypes/{ID}/optionalmoduletypes POST requests.") + public void addOptionalModuleToDistributionSetType() throws JSONException, Exception { + DistributionSetType testType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("test123", "TestName123", "Desc123")); + assertThat(testType.getOptLockRevision()).isEqualTo(1); + + mvc.perform(post("/rest/v1/distributionsettypes/{dstID}/optionalmoduletypes", testType.getId()) + .content("{\"id\":" + osType.getId() + "}").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + testType = distributionSetManagement.findDistributionSetTypeById(testType.getId()); + assertThat(testType.getLastModifiedBy()).isEqualTo("uploadTester"); + assertThat(testType.getOptLockRevision()).isEqualTo(2); + assertThat(testType.getOptionalModuleTypes()).containsExactly(osType); + assertThat(testType.getMandatoryModuleTypes()).isEmpty(); + + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/distributionsettypes/{ID}/mandatorymoduletypes GET requests.") + public void getMandatoryModulesOfDistributionSetType() throws JSONException, Exception { + final DistributionSetType testType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("test123", "TestName123", "Desc123") + .addMandatoryModuleType(osType).addOptionalModuleType(appType)); + assertThat(testType.getOptLockRevision()).isEqualTo(1); + assertThat(testType.getOptionalModuleTypes()).containsExactly(appType); + assertThat(testType.getMandatoryModuleTypes()).containsExactly(osType); + + mvc.perform(get("/rest/v1/distributionsettypes/{dstID}/mandatorymoduletypes", testType.getId()) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("[0].name", equalTo(osType.getName()))) + .andExpect(jsonPath("[0].description", equalTo(osType.getDescription()))) + .andExpect(jsonPath("[0].maxAssignments", equalTo(1))).andExpect(jsonPath("[0].key", equalTo("os"))); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/distributionsettypes/{ID}/optionalmoduletypes GET requests.") + public void getOptionalModulesOfDistributionSetType() throws JSONException, Exception { + final DistributionSetType testType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("test123", "TestName123", "Desc123") + .addMandatoryModuleType(osType).addOptionalModuleType(appType)); + assertThat(testType.getOptLockRevision()).isEqualTo(1); + assertThat(testType.getOptionalModuleTypes()).containsExactly(appType); + assertThat(testType.getMandatoryModuleTypes()).containsExactly(osType); + + mvc.perform(get("/rest/v1/distributionsettypes/{dstID}/optionalmoduletypes", testType.getId()) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("[0].name", equalTo(appType.getName()))) + .andExpect(jsonPath("[0].description", equalTo(appType.getDescription()))) + .andExpect(jsonPath("[0].maxAssignments", equalTo(1))) + .andExpect(jsonPath("[0].key", equalTo("application"))); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/distributionsettypes/{ID}/mandatorymoduletypes/{ID} GET requests.") + public void getMandatoryModuleOfDistributionSetType() throws JSONException, Exception { + final DistributionSetType testType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("test123", "TestName123", "Desc123") + .addMandatoryModuleType(osType).addOptionalModuleType(appType)); + assertThat(testType.getOptLockRevision()).isEqualTo(1); + assertThat(testType.getOptionalModuleTypes()).containsExactly(appType); + assertThat(testType.getMandatoryModuleTypes()).containsExactly(osType); + + mvc.perform(get("/rest/v1/distributionsettypes/{dstID}/mandatorymoduletypes/{smtId}", testType.getId(), + osType.getId()).contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("$id", equalTo(osType.getId().intValue()))) + .andExpect(jsonPath("$name", equalTo(osType.getName()))) + .andExpect(jsonPath("$description", equalTo(osType.getDescription()))) + .andExpect(jsonPath("$createdBy", equalTo(osType.getCreatedBy()))) + .andExpect(jsonPath("$createdAt", equalTo(osType.getCreatedAt()))) + .andExpect(jsonPath("$lastModifiedBy", equalTo(osType.getLastModifiedBy()))) + .andExpect(jsonPath("$lastModifiedAt", equalTo(osType.getLastModifiedAt()))); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/distributionsettypes/{ID}/optionalmoduletypes/{ID} GET requests.") + public void getOptionalModuleOfDistributionSetType() throws JSONException, Exception { + final DistributionSetType testType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("test123", "TestName123", "Desc123") + .addMandatoryModuleType(osType).addOptionalModuleType(appType)); + assertThat(testType.getOptLockRevision()).isEqualTo(1); + assertThat(testType.getOptionalModuleTypes()).containsExactly(appType); + assertThat(testType.getMandatoryModuleTypes()).containsExactly(osType); + + mvc.perform(get("/rest/v1/distributionsettypes/{dstID}/optionalmoduletypes/{smtId}", testType.getId(), + appType.getId()).contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("$id", equalTo(appType.getId().intValue()))) + .andExpect(jsonPath("$name", equalTo(appType.getName()))) + .andExpect(jsonPath("$description", equalTo(appType.getDescription()))) + .andExpect(jsonPath("$createdBy", equalTo(appType.getCreatedBy()))) + .andExpect(jsonPath("$createdAt", equalTo(appType.getCreatedAt()))) + .andExpect(jsonPath("$lastModifiedBy", equalTo(appType.getLastModifiedBy()))) + .andExpect(jsonPath("$lastModifiedAt", equalTo(appType.getLastModifiedAt()))); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/distributionsettypes/{ID}/mandatorymoduletypes/{ID} DELETE requests.") + public void removeMandatoryModuleToDistributionSetType() throws JSONException, Exception { + DistributionSetType testType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("test123", "TestName123", "Desc123") + .addMandatoryModuleType(osType).addOptionalModuleType(appType)); + assertThat(testType.getOptLockRevision()).isEqualTo(1); + assertThat(testType.getOptionalModuleTypes()).containsExactly(appType); + assertThat(testType.getMandatoryModuleTypes()).containsExactly(osType); + + mvc.perform(delete("/rest/v1/distributionsettypes/{dstID}/mandatorymoduletypes/{smtId}", testType.getId(), + osType.getId()).contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + testType = distributionSetManagement.findDistributionSetTypeById(testType.getId()); + assertThat(testType.getLastModifiedBy()).isEqualTo("uploadTester"); + assertThat(testType.getOptLockRevision()).isEqualTo(2); + assertThat(testType.getOptionalModuleTypes()).containsExactly(appType); + assertThat(testType.getMandatoryModuleTypes()).isEmpty(); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/distributionsettypes/{ID}/optionalmoduletypes/{ID} DELETE requests.") + public void removeOptionalModuleToDistributionSetType() throws JSONException, Exception { + DistributionSetType testType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("test123", "TestName123", "Desc123") + .addMandatoryModuleType(osType).addOptionalModuleType(appType)); + assertThat(testType.getOptLockRevision()).isEqualTo(1); + assertThat(testType.getOptionalModuleTypes()).containsExactly(appType); + assertThat(testType.getMandatoryModuleTypes()).containsExactly(osType); + + mvc.perform(delete("/rest/v1/distributionsettypes/{dstID}/optionalmoduletypes/{smtId}", testType.getId(), + appType.getId()).contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + testType = distributionSetManagement.findDistributionSetTypeById(testType.getId()); + assertThat(testType.getLastModifiedBy()).isEqualTo("uploadTester"); + assertThat(testType.getOptLockRevision()).isEqualTo(2); + assertThat(testType.getOptionalModuleTypes()).isEmpty(); + assertThat(testType.getMandatoryModuleTypes()).containsExactly(osType); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/distributionsettypes/{ID} GET requests.") + public void getDistributionSetType() throws Exception { + + DistributionSetType testType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("test123", "TestName123", "Desc123")); + testType.setDescription("Desc1234"); + testType = distributionSetManagement.updateDistributionSetType(testType); + + mvc.perform(get("/rest/v1/distributionsettypes/{dstId}", testType.getId()).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$name", equalTo("TestName123"))) + .andExpect(jsonPath("$description", equalTo("Desc1234"))) + .andExpect(jsonPath("$createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("$createdAt", equalTo(testType.getCreatedAt()))) + .andExpect(jsonPath("$lastModifiedBy", equalTo("uploadTester"))); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/DistributionSetTypes/{ID} DELETE requests (hard delete scenario).") + public void deleteDistributionSetTypeUnused() throws Exception { + final DistributionSetType testType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("test123", "TestName123", "Desc123")); + + assertThat(distributionSetManagement.countDistributionSetTypesAll()).isEqualTo(4); + + mvc.perform(delete("/rest/v1/distributionsettypes/{dsId}", testType.getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + assertThat(distributionSetManagement.countDistributionSetTypesAll()).isEqualTo(3); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/DistributionSetTypes/{ID} DELETE requests (soft delete scenario).") + public void deleteDistributionSetTypeUsed() throws Exception { + final DistributionSetType testType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("test123", "TestName123", "Desc123")); + distributionSetManagement + .createDistributionSet(new DistributionSet("sdfsd", "dsfsdf", "sdfsdf", testType, null)); + + assertThat(distributionSetManagement.countDistributionSetTypesAll()).isEqualTo(4); + assertThat(distributionSetTypeRepository.count()).isEqualTo(4); + + mvc.perform(delete("/rest/v1/distributionsettypes/{smId}", testType.getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + assertThat(distributionSetTypeRepository.count()).isEqualTo(4); + assertThat(distributionSetManagement.countDistributionSetTypesAll()).isEqualTo(3); + } + + @Test + @Description("Checks the correct behaviour of /rest/v1/distributionsettypes/{ID} PUT requests.") + public void updateDistributionSetTypeOnlyDescriptionAndNameUntouched() throws Exception { + final DistributionSetType testType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("test123", "TestName123", "Desc123")); + + final String body = new JSONObject().put("id", testType.getId()).put("description", "foobardesc") + .put("name", "nameShouldNotBeChanged").toString(); + + mvc.perform(put("/rest/v1/distributionsettypes/{smId}", testType.getId()).content(body) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$id", equalTo(testType.getId().intValue()))) + .andExpect(jsonPath("$description", equalTo("foobardesc"))) + .andExpect(jsonPath("$name", equalTo("TestName123"))).andReturn(); + } + + @Test + @Description("Checks the correct behaviour of /rest/v1/distributionsettypes GET requests with paging.") + public void getDistributionSetTypesWithoutAddtionalRequestParameters() throws Exception { + final int types = 3; + mvc.perform(get(RestConstants.DISTRIBUTIONSETTYPE_V1_REQUEST_MAPPING)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_TOTAL, equalTo(types))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_SIZE, equalTo(types))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_CONTENT, hasSize(types))); + } + + @Test + @Description("Checks the correct behaviour of /rest/v1/distributionsettypes GET requests with paging.") + public void getDistributionSetTypesWithPagingLimitRequestParameter() throws Exception { + final int types = 3; + final int limitSize = 1; + mvc.perform(get(RestConstants.DISTRIBUTIONSETTYPE_V1_REQUEST_MAPPING) + .param(RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(limitSize))) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_TOTAL, equalTo(types))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_SIZE, equalTo(limitSize))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_CONTENT, hasSize(limitSize))); + } + + @Test + @Description("Checks the correct behaviour of /rest/v1/distributionsettypes GET requests with paging.") + public void getDistributionSetTypesWithPagingLimitAndOffsetRequestParameter() throws Exception { + final int types = 3; + final int offsetParam = 2; + final int expectedSize = types - offsetParam; + mvc.perform(get(RestConstants.DISTRIBUTIONSETTYPE_V1_REQUEST_MAPPING) + .param(RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, String.valueOf(offsetParam)) + .param(RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(types))) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_TOTAL, equalTo(types))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_SIZE, equalTo(expectedSize))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_CONTENT, hasSize(expectedSize))); + } + + @Test + @Description("Ensures that the server is behaving as expected on invalid requests (wrong media type, wrong ID etc.).") + public void invalidRequestsOnDistributionSetTypesResource() throws Exception { + final DistributionSetType testType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("test123", "TestName123", "Desc123")); + + final SoftwareModuleType testSmType = softwareManagement + .createSoftwareModuleType(new SoftwareModuleType("test123", "TestName123", "Desc123", 5)); + + final List types = new ArrayList<>(); + types.add(testType); + + // DST does not exist + mvc.perform(get("/rest/v1/distributionsettypes/12345678")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + mvc.perform(get("/rest/v1/distributionsettypes/12345678/mandatorymoduletypes")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + mvc.perform(get("/rest/v1/distributionsettypes/12345678/optionalmoduletypes")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + mvc.perform(delete("/rest/v1/distributionsettypes/12345678")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + // Module types incorrect + mvc.perform(get("/rest/v1/distributionsettypes/" + standardDsType.getId() + "/mandatorymoduletypes")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + mvc.perform(get("/rest/v1/distributionsettypes/" + standardDsType.getId() + "/mandatorymoduletypes/565765656")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + mvc.perform(get("/rest/v1/distributionsettypes/" + standardDsType.getId() + "/optionalmoduletypes/565765656")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + mvc.perform(get( + "/rest/v1/distributionsettypes/" + standardDsType.getId() + "/mandatorymoduletypes/" + osType.getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + mvc.perform(get("/rest/v1/distributionsettypes/" + standardDsType.getId() + "/mandatorymoduletypes/" + + testSmType.getId())).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + mvc.perform(get( + "/rest/v1/distributionsettypes/" + standardDsType.getId() + "/optionalmoduletypes/" + osType.getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + mvc.perform(get( + "/rest/v1/distributionsettypes/" + standardDsType.getId() + "/optionalmoduletypes/" + appType.getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + mvc.perform(get("/rest/v1/distributionsettypes/" + standardDsType.getId() + "/optionalmoduletypes/" + + testSmType.getId())).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + // Modules types at creation time invalid + + final DistributionSetType testNewType = new DistributionSetType("test123", "TestName123", "Desc123"); + testNewType.addMandatoryModuleType(new SoftwareModuleType("foo", "bar", "test", Integer.MAX_VALUE)); + + mvc.perform(post("/rest/v1/distributionsettypes") + .content(JsonBuilder.distributionSetTypes(Lists.newArrayList(testNewType))) + .contentType(MediaType.APPLICATION_OCTET_STREAM)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isUnsupportedMediaType()); + + // bad request - no content + mvc.perform(post("/rest/v1/distributionsettypes").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()); + + // bad request - bad content + mvc.perform(post("/rest/v1/distributionsettypes").content("sdfjsdlkjfskdjf".getBytes()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()); + + // not allowed methods + mvc.perform(put("/rest/v1/distributionsettypes")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete("/rest/v1/distributionsettypes")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + mvc.perform(put("/rest/v1/distributionsettypes/" + standardDsType.getId() + "/mandatorymoduletypes")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + mvc.perform(put( + "/rest/v1/distributionsettypes/" + standardDsType.getId() + "/optionalmoduletypes/" + osType.getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + } + + @Test + @Description("Search erquest of software module types.") + public void searchDistributionSetTypeRsql() throws Exception { + final DistributionSetType testType = distributionSetManagement + .createDistributionSetType(new DistributionSetType("test123", "TestName123", "Desc123")); + final DistributionSetType testType2 = distributionSetManagement + .createDistributionSetType(new DistributionSetType("test1234", "TestName1234", "Desc123")); + + final String rsqlFindLikeDs1OrDs2 = "name==TestName123,name==TestName1234"; + + mvc.perform(get("/rest/v1/distributionsettypes?q=" + rsqlFindLikeDs1OrDs2)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("size", equalTo(2))) + .andExpect(jsonPath("total", equalTo(2))).andExpect(jsonPath("content[0].name", equalTo("TestName123"))) + .andExpect(jsonPath("content[1].name", equalTo("TestName1234"))); + } + + private void createSoftwareModulesAlphabetical(final int amount) { + char character = 'a'; + for (int index = 0; index < amount; index++) { + final String str = String.valueOf(character); + final SoftwareModule softwareModule = new SoftwareModule(osType, str, str, str, str); + + softwareManagement.createSoftwareModule(softwareModule); + character++; + } + } + +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/DownloadResourceTest.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/DownloadResourceTest.java new file mode 100755 index 000000000..cc8f4f6b7 --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/DownloadResourceTest.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.eclipse.hawkbit.AbstractIntegrationTestWithMongoDB; +import org.eclipse.hawkbit.MockMvcResultPrinter; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.cache.CacheConstants; +import org.eclipse.hawkbit.cache.DownloadArtifactCache; +import org.eclipse.hawkbit.cache.DownloadType; +import org.eclipse.hawkbit.repository.model.Artifact; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.cache.Cache; +import org.springframework.context.annotation.Description; + +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * + * + */ +@Features("Component Tests- Download Restful API") +@Stories("Download Resource") +public class DownloadResourceTest extends AbstractIntegrationTestWithMongoDB { + + @Autowired + @Qualifier(CacheConstants.DOWNLOAD_ID_CACHE) + private Cache downloadIdCache; + + private final String downloadIdSha1 = "downloadIdSha1"; + + private final String downloadIdNotAvailable = "downloadIdNotAvailable"; + + @Before + public void setupCache() { + + final DistributionSet distributionSet = TestDataUtil.generateDistributionSet("Test", softwareManagement, + distributionSetManagement); + final SoftwareModule softwareModule = distributionSet.getModules().stream().findFirst().get(); + final Artifact artifact = TestDataUtil.generateArtifacts(artifactManagement, softwareModule.getId()).stream() + .findFirst().get(); + + downloadIdCache.put(downloadIdSha1, new DownloadArtifactCache(DownloadType.BY_SHA1, artifact.getSha1Hash())); + } + + @Test + @Description("This test verifies the call of download artifact without a valid download id fails.") + public void testNoDownloadIdAvailable() throws Exception { + mvc.perform( + get(RestConstants.DOWNLOAD_ID_V1_REQUEST_MAPPING_BASE + RestConstants.DOWNLOAD_ID_V1_REQUEST_MAPPING, + downloadIdNotAvailable)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + } + + @Test + @Description("This test verifies the call of download artifact works and the download id will be removed.") + public void testDownload() throws Exception { + mvc.perform( + get(RestConstants.DOWNLOAD_ID_V1_REQUEST_MAPPING_BASE + RestConstants.DOWNLOAD_ID_V1_REQUEST_MAPPING, + downloadIdSha1)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + // because cache is empty + mvc.perform( + get(RestConstants.DOWNLOAD_ID_V1_REQUEST_MAPPING_BASE + RestConstants.DOWNLOAD_ID_V1_REQUEST_MAPPING, + downloadIdSha1)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + } + +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/JsonBuilder.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/JsonBuilder.java new file mode 100755 index 000000000..19728bcfa --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/JsonBuilder.java @@ -0,0 +1,263 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.RandomStringUtils; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.repository.model.Target; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Builder class for building certain json strings. + * + * + * + */ +public abstract class JsonBuilder { + + public static String softwareModules(final List modules) throws JSONException { + final StringBuilder builder = new StringBuilder(); + + builder.append("["); + int i = 0; + for (final SoftwareModule module : modules) { + try { + builder.append(new JSONObject().put("name", module.getName()) + .put("description", module.getDescription()).put("type", module.getType().getKey()) + .put("id", Long.MAX_VALUE).put("vendor", module.getVendor()).put("version", module.getVersion()) + .put("createdAt", "0").put("updatedAt", "0").put("createdBy", "fghdfkjghdfkjh") + .put("updatedBy", "fghdfkjghdfkjh").toString()); + } catch (final Exception e) { + e.printStackTrace(); + } + + if (++i < modules.size()) { + builder.append(","); + } + } + + builder.append("]"); + + return builder.toString(); + + } + + public static String softwareModuleTypes(final List types) throws JSONException { + final StringBuilder builder = new StringBuilder(); + + builder.append("["); + int i = 0; + for (final SoftwareModuleType module : types) { + try { + builder.append(new JSONObject().put("name", module.getName()) + .put("description", module.getDescription()).put("id", Long.MAX_VALUE) + .put("key", module.getKey()).put("maxAssignments", module.getMaxAssignments()) + .put("createdAt", "0").put("updatedAt", "0").put("createdBy", "fghdfkjghdfkjh") + .put("updatedBy", "fghdfkjghdfkjh").toString()); + } catch (final Exception e) { + e.printStackTrace(); + } + + if (++i < types.size()) { + builder.append(","); + } + } + + builder.append("]"); + + return builder.toString(); + + } + + /** + * builds a json string for the feedback for the execution "proceeding". + * + * @param id + * of the Action feedback refers to + * @return the built string + * @throws JSONException + */ + public static String deploymentActionInProgressFeedback(final String id) throws JSONException { + return deploymentActionFeedback(id, "proceeding"); + } + + /** + * builds a certain json string for a action feedback. + * + * @param id + * of the action the feedback refers to + * @param execution + * see ExecutionStatus + * @return the build json string + * @throws JSONException + */ + public static String deploymentActionFeedback(final String id, final String execution) throws JSONException { + return deploymentActionFeedback(id, execution, "none"); + + } + + public static String deploymentActionFeedback(final String id, final String execution, final String finished) + throws JSONException { + final List messages = new ArrayList(); + messages.add(RandomStringUtils.randomAscii(1000)); + + return new JSONObject().put("id", id).put("time", "20140511T121314") + .put("status", + new JSONObject().put("execution", execution) + .put("result", + new JSONObject().put("finished", finished).put("progress", + new JSONObject().put("cnt", 2).put("of", 5))) + .put("details", messages)) + .toString(); + + } + + /** + * @param types + * @return + */ + public static String distributionSetTypes(final List types) { + final StringBuilder builder = new StringBuilder(); + + builder.append("["); + int i = 0; + for (final DistributionSetType module : types) { + + try { + + final JSONArray osmTypes = new JSONArray(); + module.getOptionalModuleTypes().forEach(smt -> osmTypes.put(new JSONObject().put("id", smt.getId()))); + + final JSONArray msmTypes = new JSONArray(); + module.getMandatoryModuleTypes().forEach(smt -> msmTypes.put(new JSONObject().put("id", smt.getId()))); + + builder.append(new JSONObject().put("name", module.getName()) + .put("description", module.getDescription()).put("id", Long.MAX_VALUE) + .put("key", module.getKey()).put("createdAt", "0").put("updatedAt", "0") + .put("createdBy", "fghdfkjghdfkjh").put("optionalmodules", osmTypes) + .put("mandatorymodules", msmTypes).put("updatedBy", "fghdfkjghdfkjh").toString()); + } catch (final Exception e) { + e.printStackTrace(); + } + + if (++i < types.size()) { + builder.append(","); + } + } + + builder.append("]"); + + return builder.toString(); + } + + public static String distributionSets(final List sets) throws JSONException { + final StringBuilder builder = new StringBuilder(); + + builder.append("["); + int i = 0; + for (final DistributionSet set : sets) { + try { + builder.append(distributionSet(set)); + } catch (final Exception e) { + e.printStackTrace(); + } + + if (++i < sets.size()) { + builder.append(","); + } + } + + builder.append("]"); + + return builder.toString(); + + } + + public static String distributionSet(final DistributionSet set) throws JSONException { + + final List modules = set.getModules().stream().map(module -> { + try { + return new JSONObject().put("id", module.getId()); + } catch (final Exception e) { + e.printStackTrace(); + } + + return null; + }).collect(Collectors.toList()); + + return new JSONObject().put("name", set.getName()).put("description", set.getDescription()) + .put("type", set.getType().getKey()).put("id", Long.MAX_VALUE).put("version", set.getVersion()) + .put("createdAt", "0").put("updatedAt", "0").put("createdBy", "fghdfkjghdfkjh") + .put("updatedBy", "fghdfkjghdfkjh").put("requiredMigrationStep", set.isRequiredMigrationStep()) + .put("modules", modules).toString(); + + } + + /** + * @param targets + * @return + */ + public static String targets(final List targets) { + final StringBuilder builder = new StringBuilder(); + + builder.append("["); + int i = 0; + for (final Target target : targets) { + try { + builder.append(new JSONObject().put("controllerId", target.getControllerId()) + .put("description", target.getDescription()).put("name", target.getName()).put("createdAt", "0") + .put("updatedAt", "0").put("createdBy", "fghdfkjghdfkjh").put("updatedBy", "fghdfkjghdfkjh") + .toString()); + } catch (final Exception e) { + e.printStackTrace(); + } + + if (++i < targets.size()) { + builder.append(","); + } + } + + builder.append("]"); + + return builder.toString(); + } + + public static String cancelActionFeedback(final String id, final String execution) throws JSONException { + final List messages = new ArrayList(); + messages.add(RandomStringUtils.randomAscii(1000)); + return new JSONObject().put("id", id).put("time", "20140511T121314") + .put("status", + new JSONObject().put("execution", execution) + .put("result", new JSONObject().put("finished", "success")).put("details", messages)) + .toString(); + + } + + public static String configData(final String id, final Map attributes, final String execution) + throws JSONException { + return new JSONObject().put("id", id).put("time", "20140511T121314") + .put("status", + new JSONObject().put("execution", execution) + .put("result", new JSONObject().put("finished", "success")) + .put("details", new ArrayList())) + .put("data", attributes).toString(); + + } + +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/RSQLUtilityTest.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/RSQLUtilityTest.java new file mode 100755 index 000000000..a413f77e7 --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/RSQLUtilityTest.java @@ -0,0 +1,252 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import javax.persistence.EntityManager; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Expression; +import javax.persistence.criteria.Path; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import javax.persistence.metamodel.Attribute; +import javax.persistence.metamodel.ManagedType; +import javax.persistence.metamodel.Metamodel; + +import org.eclipse.hawkbit.repository.FieldNameProvider; +import org.eclipse.hawkbit.repository.SoftwareModuleFields; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.rsql.RSQLParameterSyntaxException; +import org.eclipse.hawkbit.repository.rsql.RSQLParameterUnsupportedFieldException; +import org.eclipse.hawkbit.repository.rsql.RSQLUtility; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +@RunWith(MockitoJUnitRunner.class) +@Features("Component Tests - Management RESTful API") +@Stories("RSQL search utility") +// TODO: fully document tests -> @Description for long text and reasonable +// method name as short text +public class RSQLUtilityTest { + + @Mock + private Root baseSoftwareModuleRootMock; + + @Mock + private CriteriaQuery criteriaQueryMock; + @Mock + private CriteriaBuilder criteriaBuilderMock; + @Mock + private EntityManager entityManager; + + @Mock + private Metamodel metamodel; + + @Mock + private ManagedType managedType; + + @Mock + private Attribute attribute; + + @Test(expected = RSQLParameterSyntaxException.class) + public void wrongRsqlSyntaxThrowSyntaxException() { + final String wrongRSQL = "name==abc;d"; + when(entityManager.getMetamodel()).thenReturn(metamodel); + RSQLUtility.parse(wrongRSQL, SoftwareModuleFields.class, entityManager).toPredicate(baseSoftwareModuleRootMock, + criteriaQueryMock, criteriaBuilderMock); + } + + @Test(expected = RSQLParameterUnsupportedFieldException.class) + public void wrongFieldThrowUnsupportedFieldException() { + final String wrongRSQL = "unknownField==abc"; + when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class); + doEntitySetup(SoftwareModule.class); + RSQLUtility.parse(wrongRSQL, SoftwareModuleFields.class, entityManager).toPredicate(baseSoftwareModuleRootMock, + criteriaQueryMock, criteriaBuilderMock); + } + + @Test + public void correctRsqlBuildsPredicate() { + reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); + final String correctRsql = "name==abc;version==1.2"; + when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock); + when(baseSoftwareModuleRootMock.get("version")).thenReturn(baseSoftwareModuleRootMock); + when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class); + when(criteriaBuilderMock.equal(any(Root.class), anyString())).thenReturn(mock(Predicate.class)); + when(criteriaBuilderMock. greaterThanOrEqualTo(any(Expression.class), any(String.class))).thenReturn( + mock(Predicate.class)); + + doEntitySetup(SoftwareModule.class); + + // test + RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, entityManager).toPredicate( + baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); + + // verfication + verify(criteriaBuilderMock, times(1)).and(any(Predicate.class)); + } + + @Test + public void correctRsqlBuildsNotEqualPredicate() { + reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); + final String correctRsql = "name!=abc"; + when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock); + when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class); + when(criteriaBuilderMock.equal(any(Root.class), anyString())).thenReturn(mock(Predicate.class)); + when(criteriaBuilderMock. greaterThanOrEqualTo(any(Expression.class), any(String.class))).thenReturn( + mock(Predicate.class)); + doEntitySetup(SoftwareModule.class); + // test + RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, entityManager).toPredicate( + baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); + + // verfication + verify(criteriaBuilderMock, times(1)).and(any(Predicate.class)); + verify(criteriaBuilderMock, times(1)).notEqual(eq(baseSoftwareModuleRootMock), eq("abc")); + } + + @Test + public void correctRsqlBuildsLessThanPredicate() { + reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); + final String correctRsql = "name=lt=abc"; + when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock); + when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class); + when(criteriaBuilderMock.equal(any(Root.class), anyString())).thenReturn(mock(Predicate.class)); + when(criteriaBuilderMock. greaterThanOrEqualTo(any(Expression.class), any(String.class))).thenReturn( + mock(Predicate.class)); + doEntitySetup(SoftwareModule.class); + // test + RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, entityManager).toPredicate( + baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); + + // verfication + verify(criteriaBuilderMock, times(1)).and(any(Predicate.class)); + verify(criteriaBuilderMock, times(1)).lessThan(eq(pathOfString(baseSoftwareModuleRootMock)), eq("abc")); + } + + @Test + public void correctRsqlBuildsLikePredicate() { + reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); + final String correctRsql = "name=li=abc"; + when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock); + when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class); + when(criteriaBuilderMock.equal(any(Root.class), anyString())).thenReturn(mock(Predicate.class)); + when(criteriaBuilderMock. greaterThanOrEqualTo(any(Expression.class), any(String.class))).thenReturn( + mock(Predicate.class)); + when(criteriaBuilderMock.upper(eq(pathOfString(baseSoftwareModuleRootMock)))).thenReturn( + pathOfString(baseSoftwareModuleRootMock)); + doEntitySetup(SoftwareModule.class); + // test + RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, entityManager).toPredicate( + baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); + + // verfication + verify(criteriaBuilderMock, times(1)).and(any(Predicate.class)); + verify(criteriaBuilderMock, times(1)).like(eq(pathOfString(baseSoftwareModuleRootMock)), + eq("abc".toUpperCase())); + } + + @Test + public void correctRsqlWithEnumValue() { + reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); + final String correctRsql = "testfield==bumlux"; + when(baseSoftwareModuleRootMock.get("testfield")).thenReturn(baseSoftwareModuleRootMock); + when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) TestValueEnum.class); + when(criteriaBuilderMock.equal(any(Root.class), anyString())).thenReturn(mock(Predicate.class)); + + doEntitySetup(TestValueEnum.class); + // test + RSQLUtility.parse(correctRsql, TestFieldEnum.class, entityManager).toPredicate(baseSoftwareModuleRootMock, + criteriaQueryMock, criteriaBuilderMock); + + // verfication + verify(criteriaBuilderMock, times(1)).and(any(Predicate.class)); + verify(criteriaBuilderMock, times(1)).equal(eq(baseSoftwareModuleRootMock), eq(TestValueEnum.BUMLUX)); + } + + @Test + public void wrongRsqlWithWrongEnumValue() { + reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); + final String correctRsql = "testfield==unknownValue"; + when(baseSoftwareModuleRootMock.get("testfield")).thenReturn(baseSoftwareModuleRootMock); + when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) TestValueEnum.class); + when(criteriaBuilderMock.equal(any(Root.class), anyString())).thenReturn(mock(Predicate.class)); + + doEntitySetup(TestValueEnum.class); + + try { + // test + RSQLUtility.parse(correctRsql, TestFieldEnum.class, entityManager).toPredicate(baseSoftwareModuleRootMock, + criteriaQueryMock, criteriaBuilderMock); + fail("missing RSQLParameterUnsupportedFieldException for wrong enum value"); + } catch (final RSQLParameterUnsupportedFieldException e) { + // nope expected + } + } + + private void doEntitySetup(final Class clasName) { + when(entityManager.getMetamodel()).thenReturn(metamodel); + when(metamodel.managedType(clasName)).thenReturn(managedType); + when(managedType.getJavaType()).thenReturn(clasName); + + doAnswer(new Answer() { + @Override + public Attribute answer(final InvocationOnMock invocation) throws Throwable { + return attribute; + } + }).when(managedType).getAttribute(anyString()); + + when(attribute.isAssociation()).thenReturn(false); + + } + + @SuppressWarnings("unchecked") + private Path pathOfString(final Path path) { + return (Path) path; + } + + private enum TestFieldEnum implements FieldNameProvider { + TESTFIELD; + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.rest.resource.model.FieldNameProvider# + * getFieldName() + */ + @Override + public String getFieldName() { + return "testfield"; + } + } + + private enum TestValueEnum { + BUMLUX; + } + +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/ResourceUtility.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/ResourceUtility.java new file mode 100755 index 000000000..9e99031e9 --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/ResourceUtility.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import java.io.IOException; + +import org.eclipse.hawkbit.rest.resource.model.ExceptionInfo; +import org.eclipse.hawkbit.rest.resource.model.PagedList; +import org.eclipse.hawkbit.rest.resource.model.artifact.ArtifactRest; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Utility additions for the REST API tests. + * + * + * + * + */ +public final class ResourceUtility { + private static final ObjectMapper mapper = new ObjectMapper(); + + static ExceptionInfo convertException(final String jsonExceptionResponse) + throws JsonParseException, JsonMappingException, IOException { + return mapper.readValue(jsonExceptionResponse, ExceptionInfo.class); + } + + static ArtifactRest convertArtifactResponse(final String jsonResponse) + throws JsonParseException, JsonMappingException, IOException { + return mapper.readValue(jsonResponse, ArtifactRest.class); + + } + + static PagedList mapResponse(final Class clazz, final String responseBody) + throws JsonParseException, JsonMappingException, IOException { + return mapper.readValue(responseBody, PagedList.class); + } + +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/SMRessourceMisingMongoDbConnectionTest.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/SMRessourceMisingMongoDbConnectionTest.java new file mode 100755 index 000000000..f9be088a4 --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/SMRessourceMisingMongoDbConnectionTest.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.apache.commons.lang3.RandomStringUtils; +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.MockMvcResultPrinter; +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.rest.resource.model.ExceptionInfo; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MvcResult; + +/** + * Tests {@link SoftwareModuleResource} in case of missing MongoDB connection. + * + * + * + * + */ +public class SMRessourceMisingMongoDbConnectionTest extends AbstractIntegrationTest { + + @BeforeClass + public static void initialize() { + // set property to mongoPort which does not start any mongoDB of + // parallel test execution + System.setProperty("spring.data.mongodb.port", "1020"); + } + + @Test + public void testMissingMongoDbConnection() throws Exception { + + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + assertThat(artifactRepository.findAll()).hasSize(0); + SoftwareModule sm = new SoftwareModule(softwareManagement.findSoftwareModuleTypeByKey("os"), "name 1", + "version 1", null, null); + sm = softwareManagement.createSoftwareModule(sm); + assertThat(artifactRepository.findAll()).hasSize(0); + + // create test file + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + final MockMultipartFile file = new MockMultipartFile("file", "origFilename", null, random); + + // upload + final MvcResult mvcResult = mvc + .perform(fileUpload("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId()).file(file)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isInternalServerError()).andReturn(); + + // check error result + final ExceptionInfo exceptionInfo = ResourceUtility + .convertException(mvcResult.getResponse().getContentAsString()); + assertThat(exceptionInfo.getErrorCode()).isEqualTo(SpServerError.SP_ARTIFACT_UPLOAD_FAILED.getKey()); + assertThat(exceptionInfo.getMessage()).isEqualTo(SpServerError.SP_ARTIFACT_UPLOAD_FAILED.getMessage()); + + // ensure that the JPA transaction was rolled back + assertThat(artifactRepository.findAll()).hasSize(0); + + } + +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleResourceTest.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleResourceTest.java new file mode 100755 index 000000000..cfca895a6 --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleResourceTest.java @@ -0,0 +1,1008 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.eclipse.hawkbit.AbstractIntegrationTestWithMongoDB; +import org.eclipse.hawkbit.HashGeneratorUtils; +import org.eclipse.hawkbit.MockMvcResultPrinter; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.WithUser; +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.Artifact; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata; +import org.eclipse.hawkbit.repository.model.SwMetadataCompositeKey; +import org.eclipse.hawkbit.rest.resource.model.ExceptionInfo; +import org.eclipse.hawkbit.rest.resource.model.artifact.ArtifactRest; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.web.bind.annotation.RestController; + +import com.jayway.jsonpath.JsonPath; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * Tests for {@link SoftwareModuleResource} {@link RestController}. + * + * + * + */ +@Features("Component Tests - Management RESTful API") +@Stories("Software Module Resource") +// TODO: fully document tests -> @Description for long text and reasonable +// method name as short text +public class SoftwareModuleResourceTest extends AbstractIntegrationTestWithMongoDB { + + @Test + public void updateSoftwareModuleOnlyDescriptionAndVendorNameUntouched() throws Exception { + final String knownSWName = "name1"; + final String knownSWVersion = "version1"; + final String knownSWDescription = "description1"; + final String knownSWVendor = "vendor1"; + + final String updateVendor = "newVendor1"; + final String updateDescription = "newDescription1"; + + final SoftwareModule ah = softwareManagement + .createSoftwareModule(new SoftwareModule(appType, "agent-hub", "1.0.1", null, "")); + final SoftwareModule jvm = softwareManagement + .createSoftwareModule(new SoftwareModule(runtimeType, "oracle-jre", "1.7.2", null, "")); + final SoftwareModule os = softwareManagement + .createSoftwareModule(new SoftwareModule(osType, "poky", "3.0.2", null, "")); + + SoftwareModule sm = new SoftwareModule(osType, knownSWName, knownSWVersion, knownSWDescription, knownSWVendor); + sm = softwareManagement.createSoftwareModule(sm); + + assertThat(sm.getName()).isEqualTo(knownSWName); + assertThat(sm.getName()).isEqualTo(knownSWName); + + final String body = new JSONObject().put("vendor", updateVendor).put("description", updateDescription) + .put("name", "nameShouldNotBeChanged").toString(); + + mvc.perform(put("/rest/v1/softwaremodules/{smId}", sm.getId()).content(body) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$id", equalTo(sm.getId().intValue()))) + .andExpect(jsonPath("$vendor", equalTo(updateVendor))) + .andExpect(jsonPath("$description", equalTo(updateDescription))) + .andExpect(jsonPath("$name", equalTo(knownSWName))).andReturn(); + + } + + /** + * Test method for + * {@link org.eclipse.hawkbit.rest.resource.SoftwareModuleResource#uploadArtifact(java.lang.Long, org.springframework.web.multipart.MultipartFile)} + * . + * + * @throws Exception + * if test fails + */ + @Test + public void testUploadArtifact() throws Exception { + // prepare repo + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + assertThat(artifactRepository.findAll()).hasSize(0); + SoftwareModule sm = new SoftwareModule(osType, "name 1", "version 1", null, null); + sm = softwareManagement.createSoftwareModule(sm); + assertThat(artifactRepository.findAll()).hasSize(0); + + // create test file + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + final String md5sum = HashGeneratorUtils.generateMD5(random); + final String sha1sum = HashGeneratorUtils.generateSHA1(random); + final MockMultipartFile file = new MockMultipartFile("file", "origFilename", null, random); + + // upload + final MvcResult mvcResult = mvc + .perform(fileUpload("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId()).file(file) + .accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$type", equalTo("local"))).andExpect(jsonPath("$hashes.md5", equalTo(md5sum))) + .andExpect(jsonPath("$hashes.sha1", equalTo(sha1sum))) + .andExpect(jsonPath("$providedFilename", equalTo("origFilename"))).andReturn(); + + // check rest of response compared to DB + final ArtifactRest artResult = ResourceUtility + .convertArtifactResponse(mvcResult.getResponse().getContentAsString()); + final Long artId = ((LocalArtifact) softwareManagement.findSoftwareModuleWithDetails(sm.getId()).getArtifacts() + .get(0)).getId(); + assertThat(artResult.getArtifactId()).isEqualTo(artId); + assertThat(JsonPath.compile("$_links.self.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/softwaremodules/" + sm.getId() + "/artifacts/" + artId); + assertThat( + JsonPath.compile("$_links.download.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/softwaremodules/" + sm.getId() + "/artifacts/" + artId + + "/download"); + + // check result in db... + // repo + assertThat(artifactRepository.findAll()).hasSize(1); + + // binary + assertTrue(IOUtils.contentEquals(new ByteArrayInputStream(random), + artifactManagement + .loadLocalArtifactBinary((LocalArtifact) softwareManagement + .findSoftwareModuleWithDetails(sm.getId()).getArtifacts().get(0)) + .getFileInputStream())); + + // hashes + assertThat(artifactManagement.findLocalArtifactByFilename("origFilename").get(0).getSha1Hash()) + .isEqualTo(HashGeneratorUtils.generateSHA1(random)); + + assertThat(artifactManagement.findLocalArtifactByFilename("origFilename").get(0).getMd5Hash()) + .isEqualTo(HashGeneratorUtils.generateMD5(random)); + + // metadata + assertThat(((LocalArtifact) softwareManagement.findSoftwareModuleWithDetails(sm.getId()).getArtifacts().get(0)) + .getFilename()).isEqualTo("origFilename"); + + } + + @Test + public void testEmptyUploadArtifact() throws Exception { + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + assertThat(artifactRepository.findAll()).hasSize(0); + + SoftwareModule sm = new SoftwareModule(osType, "name 1", "version 1", null, null); + sm = softwareManagement.createSoftwareModule(sm); + + final MockMultipartFile file = new MockMultipartFile("file", "orig", null, new byte[0]); + + mvc.perform(fileUpload("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId()).file(file) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()); + } + + @Test + public void testDuplicateUploadArtifact() throws Exception { + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + assertThat(artifactRepository.findAll()).hasSize(0); + + SoftwareModule sm = new SoftwareModule(osType, "name 1", "version 1", null, null); + sm = softwareManagement.createSoftwareModule(sm); + + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + final String md5sum = HashGeneratorUtils.generateMD5(random); + final String sha1sum = HashGeneratorUtils.generateSHA1(random); + final MockMultipartFile file = new MockMultipartFile("file", "orig", null, random); + + mvc.perform(fileUpload("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId()).file(file) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$type", equalTo("local"))).andExpect(jsonPath("$hashes.md5", equalTo(md5sum))) + .andExpect(jsonPath("$hashes.sha1", equalTo(sha1sum))) + .andExpect(jsonPath("$providedFilename", equalTo("orig"))).andExpect(status().isCreated()); + + mvc.perform(fileUpload("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId()).file(file)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isConflict()); + } + + @Test + public void testUploadArtifactWithCustomName() throws Exception { + // prepare repo + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + assertThat(artifactRepository.findAll()).hasSize(0); + SoftwareModule sm = new SoftwareModule(osType, "name 1", "version 1", null, null); + sm = softwareManagement.createSoftwareModule(sm); + assertThat(artifactRepository.findAll()).hasSize(0); + + // create test file + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + final MockMultipartFile file = new MockMultipartFile("file", "origFilename", null, random); + + // upload + mvc.perform(fileUpload("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId()).file(file) + .param("filename", "customFilename").accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$providedFilename", equalTo("customFilename"))).andExpect(status().isCreated()); + ; + + // check result in db... + // repo + assertThat(artifactRepository.findAll()).hasSize(1); + + // hashes + assertThat(artifactManagement.findLocalArtifactByFilename("customFilename")).hasSize(1); + } + + @Test + public void testUploadArtifactWithHashCheck() throws Exception { + // prepare repo + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + assertThat(artifactRepository.findAll()).hasSize(0); + SoftwareModule sm = new SoftwareModule(osType, "name 1", "version 1", null, null); + sm = softwareManagement.createSoftwareModule(sm); + assertThat(artifactRepository.findAll()).hasSize(0); + + // create test file + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + final String md5sum = HashGeneratorUtils.generateMD5(random); + final String sha1sum = HashGeneratorUtils.generateSHA1(random); + final MockMultipartFile file = new MockMultipartFile("file", "origFilename", null, random); + + // upload + // wrong sha1 + MvcResult mvcResult = mvc + .perform(fileUpload("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId()).file(file) + .param("md5sum", md5sum).param("sha1sum", "afsdff")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()).andReturn(); + + // check error result + ExceptionInfo exceptionInfo = ResourceUtility.convertException(mvcResult.getResponse().getContentAsString()); + assertThat(exceptionInfo.getErrorCode()).isEqualTo(SpServerError.SP_ARTIFACT_UPLOAD_FAILED_SHA1_MATCH.getKey()); + + // wrong md5 + mvcResult = mvc + .perform(fileUpload("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId()).file(file) + .param("md5sum", "sdfsdfs").param("sha1sum", sha1sum)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()).andReturn(); + + // check error result + exceptionInfo = ResourceUtility.convertException(mvcResult.getResponse().getContentAsString()); + assertThat(exceptionInfo.getErrorCode()).isEqualTo(SpServerError.SP_ARTIFACT_UPLOAD_FAILED_MD5_MATCH.getKey()); + + mvc.perform(fileUpload("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId()).file(file) + .param("md5sum", md5sum).param("sha1sum", sha1sum)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isCreated()); + + // check result... + // repo + assertThat(artifactRepository.findAll()).hasSize(1); + + // binary + assertTrue(IOUtils.contentEquals(new ByteArrayInputStream(random), + artifactManagement + .loadLocalArtifactBinary((LocalArtifact) softwareManagement + .findSoftwareModuleWithDetails(sm.getId()).getArtifacts().get(0)) + .getFileInputStream())); + + // hashes + assertThat(artifactManagement.findLocalArtifactByFilename("origFilename").get(0).getSha1Hash()) + .isEqualTo(HashGeneratorUtils.generateSHA1(random)); + + assertThat(artifactManagement.findLocalArtifactByFilename("origFilename").get(0).getMd5Hash()) + .isEqualTo(md5sum); + + // metadata + assertThat(((LocalArtifact) softwareManagement.findSoftwareModuleWithDetails(sm.getId()).getArtifacts().get(0)) + .getFilename()).isEqualTo("origFilename"); + + } + + /** + * Test method for + * {@link org.eclipse.hawkbit.rest.resource.SoftwareModuleResource#downloadArtifact(java.lang.Long, javax.servlet.http.HttpServletResponse)} + * . + * + * @throws Exception + * if test fails + */ + @Test + public void testDownloadArtifact() throws Exception { + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + assertThat(artifactRepository.findAll()).hasSize(0); + + SoftwareModule sm = new SoftwareModule(osType, "name 1", "version 1", null, null); + sm = softwareManagement.createSoftwareModule(sm); + + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + + final Artifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), sm.getId(), + "file1", false); + final Artifact artifact2 = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), sm.getId(), + "file2", false); + + final MvcResult result = mvc + .perform( + get("/rest/v1/softwaremodules/{smId}/artifacts/{artId}/download", sm.getId(), artifact.getId())) + .andExpect(header().string("ETag", artifact.getSha1Hash())).andReturn(); + + assertTrue(Arrays.equals(result.getResponse().getContentAsByteArray(), random)); + + final MvcResult result2 = mvc.perform( + get("/rest/v1/softwaremodules/{smId}/artifacts/{artId}/download", sm.getId(), artifact2.getId())) + .andExpect(header().string("ETag", artifact2.getSha1Hash())).andReturn(); + + assertTrue(Arrays.equals(result2.getResponse().getContentAsByteArray(), random)); + + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(1); + assertThat(artifactRepository.findAll()).hasSize(2); + } + + @Test + public void testGetArtifact() throws Exception { + // check baseline + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + assertThat(artifactRepository.findAll()).hasSize(0); + + // prepare data for test + SoftwareModule sm = new SoftwareModule(osType, "name 1", "version 1", null, null); + sm = softwareManagement.createSoftwareModule(sm); + + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + final Artifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), sm.getId(), + "file1", false); + + // perform test + mvc.perform(get("/rest/v1/softwaremodules/{smId}/artifacts/{artId}", sm.getId(), artifact.getId()) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$id", equalTo(artifact.getId().intValue()))) + .andExpect(jsonPath("$type", equalTo("local"))).andExpect(jsonPath("$size", equalTo(random.length))) + .andExpect(jsonPath("$hashes.md5", equalTo(artifact.getMd5Hash()))) + .andExpect(jsonPath("$hashes.sha1", equalTo(artifact.getSha1Hash()))) + .andExpect(jsonPath("$providedFilename", equalTo("file1"))) + .andExpect(jsonPath("$_links.download.href", + equalTo("http://localhost/rest/v1/softwaremodules/" + sm.getId() + "/artifacts/" + + artifact.getId() + "/download"))) + .andExpect(jsonPath("$_links.self.href", equalTo( + "http://localhost/rest/v1/softwaremodules/" + sm.getId() + "/artifacts/" + artifact.getId()))); + } + + @Test + public void testGetArtifacts() throws Exception { + SoftwareModule sm = new SoftwareModule(osType, "name 1", "version 1", null, null); + sm = softwareManagement.createSoftwareModule(sm); + + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + + final Artifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), sm.getId(), + "file1", false); + final Artifact artifact2 = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), sm.getId(), + "file2", false); + + mvc.perform(get("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId()).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$[0].id", equalTo(artifact.getId().intValue()))) + .andExpect(jsonPath("$[0].type", equalTo("local"))) + .andExpect(jsonPath("$[0].size", equalTo(random.length))) + .andExpect(jsonPath("$[0].hashes.md5", equalTo(artifact.getMd5Hash()))) + .andExpect(jsonPath("$[0].hashes.sha1", equalTo(artifact.getSha1Hash()))) + .andExpect(jsonPath("$[0].providedFilename", equalTo("file1"))) + .andExpect(jsonPath("$[0]._links.download.href", + equalTo("http://localhost/rest/v1/softwaremodules/" + sm.getId() + "/artifacts/" + + artifact.getId() + "/download"))) + .andExpect(jsonPath("$[0]._links.self.href", + equalTo("http://localhost/rest/v1/softwaremodules/" + sm.getId() + "/artifacts/" + + artifact.getId()))) + .andExpect(jsonPath("$[1].id", equalTo(artifact2.getId().intValue()))) + .andExpect(jsonPath("$[1].type", equalTo("local"))) + .andExpect(jsonPath("$[1].hashes.md5", equalTo(artifact2.getMd5Hash()))) + .andExpect(jsonPath("$[1].hashes.sha1", equalTo(artifact2.getSha1Hash()))) + .andExpect(jsonPath("$[1].providedFilename", equalTo("file2"))) + .andExpect(jsonPath("$[1]._links.download.href", + equalTo("http://localhost/rest/v1/softwaremodules/" + sm.getId() + "/artifacts/" + + artifact2.getId() + "/download"))) + .andExpect(jsonPath("$[1]._links.self.href", equalTo( + "http://localhost/rest/v1/softwaremodules/" + sm.getId() + "/artifacts/" + artifact2.getId()))); + } + + @Test + public void testInvalidRequestsOnArtifactResource() throws Exception { + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + final MockMultipartFile file = new MockMultipartFile("file", "orig", null, random); + + SoftwareModule sm = new SoftwareModule(osType, "name 1", "version 1", null, null); + sm = softwareManagement.createSoftwareModule(sm); + + // no artifact available + mvc.perform(get("/rest/v1/softwaremodules/{smId}/artifacts/1234567/download", sm.getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + mvc.perform(delete("/rest/v1/softwaremodules/{smId}/artifacts/1234567", sm.getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + // SM does not exist + artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), sm.getId(), "file1", false); + mvc.perform(get("/rest/v1/softwaremodules/1234567890/artifacts")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + mvc.perform(fileUpload("/rest/v1/softwaremodules/1234567890/artifacts").file(file)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + // bad request - no content + mvc.perform(fileUpload("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()); + + // not allowed methods + mvc.perform(put("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + } + + @Test + public void testInvalidRequestsOnSoftwaremodulesResource() throws Exception { + SoftwareModule sm = new SoftwareModule(osType, "name 1", "version 1", null, null); + sm = softwareManagement.createSoftwareModule(sm); + + final List modules = new ArrayList<>(); + modules.add(sm); + + // SM does not exist + mvc.perform(get("/rest/v1/softwaremodules/12345678")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + mvc.perform(delete("/rest/v1/softwaremodules/12345678")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + // bad request - no content + mvc.perform(post("/rest/v1/softwaremodules").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()); + + // bad request - bad content + mvc.perform(post("/rest/v1/softwaremodules").content("sdfjsdlkjfskdjf".getBytes()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()); + + // unsupported media type + mvc.perform(post("/rest/v1/softwaremodules").content(JsonBuilder.softwareModules(modules)) + .contentType(MediaType.APPLICATION_OCTET_STREAM)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isUnsupportedMediaType()); + + // not allowed methods + mvc.perform(put("/rest/v1/softwaremodules")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete("/rest/v1/softwaremodules")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + } + + @Test + public void testGetSoftwareModulesWithoutAddtionalRequestParameters() throws Exception { + final int modules = 5; + createSoftwareModulesAlphabetical(modules); + mvc.perform(get(RestConstants.SOFTWAREMODULE_V1_REQUEST_MAPPING)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_TOTAL, equalTo(modules))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_SIZE, equalTo(modules))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_CONTENT, hasSize(modules))); + } + + @Test + public void testGetSoftwareModulesWithPagingLimitRequestParameter() throws Exception { + final int modules = 5; + final int limitSize = 1; + createSoftwareModulesAlphabetical(modules); + mvc.perform(get(RestConstants.SOFTWAREMODULE_V1_REQUEST_MAPPING) + .param(RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(limitSize))) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_TOTAL, equalTo(modules))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_SIZE, equalTo(limitSize))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_CONTENT, hasSize(limitSize))); + } + + @Test + public void testGetSoftwareModulesWithPagingLimitAndOffsetRequestParameter() throws Exception { + final int modules = 5; + final int offsetParam = 2; + final int expectedSize = modules - offsetParam; + createSoftwareModulesAlphabetical(modules); + mvc.perform(get(RestConstants.SOFTWAREMODULE_V1_REQUEST_MAPPING) + .param(RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, String.valueOf(offsetParam)) + .param(RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(modules))) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_TOTAL, equalTo(modules))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_SIZE, equalTo(expectedSize))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_CONTENT, hasSize(expectedSize))); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + public void testGetSoftwareModules() throws Exception { + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + + SoftwareModule os = new SoftwareModule(osType, "name1", "version1", "description1", "vendor1"); + os = softwareManagement.createSoftwareModule(os); + + SoftwareModule jvm = new SoftwareModule(runtimeType, "name1", "version1", "description1", "vendor1"); + jvm = softwareManagement.createSoftwareModule(jvm); + + SoftwareModule ah = new SoftwareModule(appType, "name1", "version1", "description1", "vendor1"); + ah = softwareManagement.createSoftwareModule(ah); + + mvc.perform(get("/rest/v1/softwaremodules").accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$content.[?(@.id==" + os.getId() + ")][0].name", equalTo("name1"))) + .andExpect(jsonPath("$content.[?(@.id==" + os.getId() + ")][0].version", equalTo("version1"))) + .andExpect(jsonPath("$content.[?(@.id==" + os.getId() + ")][0].description", equalTo("description1"))) + .andExpect(jsonPath("$content.[?(@.id==" + os.getId() + ")][0].vendor", equalTo("vendor1"))) + .andExpect(jsonPath("$content.[?(@.id==" + os.getId() + ")][0].type", equalTo("os"))) + .andExpect(jsonPath("$content.[?(@.id==" + os.getId() + ")][0].createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("$content.[?(@.id==" + os.getId() + ")][0].createdAt", equalTo(os.getCreatedAt()))) + .andExpect(jsonPath("$content.[?(@.id==" + os.getId() + ")][0]._links.type.href", + equalTo("http://localhost/rest/v1/softwaremoduletypes/" + osType.getId()))) + .andExpect(jsonPath("$content.[?(@.id==" + os.getId() + ")][0]._links.self.href", + equalTo("http://localhost/rest/v1/softwaremodules/" + os.getId()))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm.getId() + ")][0].name", equalTo("name1"))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm.getId() + ")][0].version", equalTo("version1"))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm.getId() + ")][0].description", equalTo("description1"))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm.getId() + ")][0].vendor", equalTo("vendor1"))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm.getId() + ")][0].type", equalTo("runtime"))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm.getId() + ")][0].createdBy", equalTo("uploadTester"))) + .andExpect( + jsonPath("$content.[?(@.id==" + jvm.getId() + ")][0].createdAt", equalTo(jvm.getCreatedAt()))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm.getId() + ")][0]._links.type.href", + equalTo("http://localhost/rest/v1/softwaremoduletypes/" + runtimeType.getId()))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm.getId() + ")][0]._links.self.href", + equalTo("http://localhost/rest/v1/softwaremodules/" + jvm.getId()))) + .andExpect(jsonPath("$content.[?(@.id==" + ah.getId() + ")][0].name", equalTo("name1"))) + .andExpect(jsonPath("$content.[?(@.id==" + ah.getId() + ")][0].version", equalTo("version1"))) + .andExpect(jsonPath("$content.[?(@.id==" + ah.getId() + ")][0].description", equalTo("description1"))) + .andExpect(jsonPath("$content.[?(@.id==" + ah.getId() + ")][0].vendor", equalTo("vendor1"))) + .andExpect(jsonPath("$content.[?(@.id==" + ah.getId() + ")][0].type", equalTo("application"))) + .andExpect(jsonPath("$content.[?(@.id==" + ah.getId() + ")][0].createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("$content.[?(@.id==" + ah.getId() + ")][0].createdAt", equalTo(ah.getCreatedAt()))) + .andExpect(jsonPath("$content.[?(@.id==" + ah.getId() + ")][0]._links.type.href", + equalTo("http://localhost/rest/v1/softwaremoduletypes/" + appType.getId()))) + .andExpect(jsonPath("$content.[?(@.id==" + ah.getId() + ")][0]._links.self.href", + equalTo("http://localhost/rest/v1/softwaremodules/" + ah.getId()))); + + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(3); + } + + @Test + public void testGetSoftwareModulesWithFilterParameters() throws Exception { + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + + SoftwareModule os1 = new SoftwareModule(osType, "osName1", "1.0.0", "description1", "vendor1"); + os1 = softwareManagement.createSoftwareModule(os1); + + SoftwareModule jvm1 = new SoftwareModule(runtimeType, "runtimeName1", "2.0.0", "description1", "vendor1"); + jvm1 = softwareManagement.createSoftwareModule(jvm1); + + SoftwareModule ah1 = new SoftwareModule(appType, "appName1", "3.0.0", "description1", "vendor1"); + ah1 = softwareManagement.createSoftwareModule(ah1); + + SoftwareModule os2 = new SoftwareModule(osType, "osName2", "1.0.1", "description2", "vendor2"); + os2 = softwareManagement.createSoftwareModule(os2); + + SoftwareModule jvm2 = new SoftwareModule(runtimeType, "runtimeName2", "2.0.1", "description2", "vendor2"); + jvm2 = softwareManagement.createSoftwareModule(jvm2); + + SoftwareModule ah2 = new SoftwareModule(appType, "appName2", "3.0.1", "description2", "vendor2"); + ah2 = softwareManagement.createSoftwareModule(ah2); + + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(6); + + // only by name, only one exists per name + mvc.perform(get("/rest/v1/softwaremodules?q=name==osName1").accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$content.[?(@.id==" + os1.getId() + ")][0].name", equalTo("osName1"))) + .andExpect(jsonPath("$content.[?(@.id==" + os1.getId() + ")][0].version", equalTo("1.0.0"))) + .andExpect(jsonPath("$content.[?(@.id==" + os1.getId() + ")][0].description", equalTo("description1"))) + .andExpect(jsonPath("$content.[?(@.id==" + os1.getId() + ")][0].vendor", equalTo("vendor1"))) + .andExpect(jsonPath("$content.[?(@.id==" + os1.getId() + ")][0].type", equalTo("os"))) + .andExpect(jsonPath("$content", hasSize(1))).andExpect(jsonPath("$total", equalTo(1))); + + // by type, 2 software modules per type exists + mvc.perform(get("/rest/v1/softwaremodules?q=type==application").accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$content.[?(@.id==" + ah1.getId() + ")][0].name", equalTo("appName1"))) + .andExpect(jsonPath("$content.[?(@.id==" + ah1.getId() + ")][0].version", equalTo("3.0.0"))) + .andExpect(jsonPath("$content.[?(@.id==" + ah1.getId() + ")][0].description", equalTo("description1"))) + .andExpect(jsonPath("$content.[?(@.id==" + ah1.getId() + ")][0].vendor", equalTo("vendor1"))) + .andExpect(jsonPath("$content.[?(@.id==" + ah1.getId() + ")][0].type", equalTo("application"))) + .andExpect(jsonPath("$content.[?(@.id==" + ah2.getId() + ")][0].name", equalTo("appName2"))) + .andExpect(jsonPath("$content.[?(@.id==" + ah2.getId() + ")][0].version", equalTo("3.0.1"))) + .andExpect(jsonPath("$content.[?(@.id==" + ah2.getId() + ")][0].description", equalTo("description2"))) + .andExpect(jsonPath("$content.[?(@.id==" + ah2.getId() + ")][0].vendor", equalTo("vendor2"))) + .andExpect(jsonPath("$content.[?(@.id==" + ah2.getId() + ")][0].type", equalTo("application"))) + .andExpect(jsonPath("$content", hasSize(2))).andExpect(jsonPath("$total", equalTo(2))); + + // by type and version=2.0.0 -> only one result + mvc.perform(get("/rest/v1/softwaremodules?q=type==runtime;version==2.0.0").accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$content.[?(@.id==" + jvm1.getId() + ")][0].name", equalTo("runtimeName1"))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm1.getId() + ")][0].version", equalTo("2.0.0"))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm1.getId() + ")][0].description", equalTo("description1"))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm1.getId() + ")][0].vendor", equalTo("vendor1"))) + .andExpect(jsonPath("$content", hasSize(1))).andExpect(jsonPath("$total", equalTo(1))); + + // by type and version range >=2.0.0 -> 2 result + mvc.perform(get("/rest/v1/softwaremodules?q=type==runtime;version=ge=2.0.0").accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$content.[?(@.id==" + jvm1.getId() + ")][0].name", equalTo("runtimeName1"))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm1.getId() + ")][0].version", equalTo("2.0.0"))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm1.getId() + ")][0].description", equalTo("description1"))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm1.getId() + ")][0].vendor", equalTo("vendor1"))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm2.getId() + ")][0].name", equalTo("runtimeName2"))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm2.getId() + ")][0].version", equalTo("2.0.1"))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm2.getId() + ")][0].description", equalTo("description2"))) + .andExpect(jsonPath("$content.[?(@.id==" + jvm2.getId() + ")][0].vendor", equalTo("vendor2"))) + .andExpect(jsonPath("$content", hasSize(2))).andExpect(jsonPath("$total", equalTo(2))); + } + + @Test + public void testGetSoftwareModulesWithSyntaxErrorFilterParameter() throws Exception { + mvc.perform(get("/rest/v1/softwaremodules?q=wrongFIQLSyntax").accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()) + .andExpect(jsonPath("$errorCode", equalTo("hawkbit.server.error.rest.param.rsqlParamSyntax"))); + } + + @Test + public void testGetSoftwareModulesWithUnknownFieldErrorFilterParameter() throws Exception { + mvc.perform(get("/rest/v1/softwaremodules?q=wrongField==abc").accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()) + .andExpect(jsonPath("$errorCode", equalTo("hawkbit.server.error.rest.param.rsqlInvalidField"))); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Tests GET request on /rest/v1/softwaremodules/{smId}.") + public void getSoftareModule() throws Exception { + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + + SoftwareModule os = new SoftwareModule(osType, "name1", "version1", "description1", "vendor1"); + os = softwareManagement.createSoftwareModule(os); + + mvc.perform(get("/rest/v1/softwaremodules/{smId}", os.getId()).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$name", equalTo("name1"))).andExpect(jsonPath("$version", equalTo("version1"))) + .andExpect(jsonPath("$description", equalTo("description1"))) + .andExpect(jsonPath("$vendor", equalTo("vendor1"))).andExpect(jsonPath("$type", equalTo("os"))) + .andExpect(jsonPath("$createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("$createdAt", equalTo(os.getCreatedAt()))) + .andExpect(jsonPath("$_links.type.href", + equalTo("http://localhost/rest/v1/softwaremoduletypes/" + osType.getId()))) + .andExpect(jsonPath("$_links.artifacts.href", + equalTo("http://localhost/rest/v1/softwaremodules/" + os.getId() + "/artifacts"))); + + SoftwareModule jvm = new SoftwareModule(runtimeType, "name1", "version1", "description1", "vendor1"); + jvm = softwareManagement.createSoftwareModule(jvm); + + mvc.perform(get("/rest/v1/softwaremodules/{smId}", jvm.getId()).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$name", equalTo("name1"))).andExpect(jsonPath("$version", equalTo("version1"))) + .andExpect(jsonPath("$description", equalTo("description1"))) + .andExpect(jsonPath("$vendor", equalTo("vendor1"))).andExpect(jsonPath("$type", equalTo("runtime"))) + .andExpect(jsonPath("$createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("$createdAt", equalTo(jvm.getCreatedAt()))) + .andExpect(jsonPath("$_links.type.href", + equalTo("http://localhost/rest/v1/softwaremoduletypes/" + runtimeType.getId()))) + .andExpect(jsonPath("$_links.artifacts.href", + equalTo("http://localhost/rest/v1/softwaremodules/" + jvm.getId() + "/artifacts"))); + + SoftwareModule ah = new SoftwareModule(appType, "name1", "version1", "description1", "vendor1"); + ah = softwareManagement.createSoftwareModule(ah); + + mvc.perform(get("/rest/v1/softwaremodules/{smId}", ah.getId()).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$name", equalTo("name1"))).andExpect(jsonPath("$version", equalTo("version1"))) + .andExpect(jsonPath("$description", equalTo("description1"))) + .andExpect(jsonPath("$vendor", equalTo("vendor1"))).andExpect(jsonPath("$type", equalTo("application"))) + .andExpect(jsonPath("$createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("$createdAt", equalTo(ah.getCreatedAt()))) + .andExpect(jsonPath("$_links.type.href", + equalTo("http://localhost/rest/v1/softwaremoduletypes/" + appType.getId()))) + .andExpect(jsonPath("$_links.artifacts.href", + equalTo("http://localhost/rest/v1/softwaremodules/" + ah.getId() + "/artifacts"))); + + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(3); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + public void createSoftwareModules() throws JSONException, Exception { + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + + final SoftwareModule os = new SoftwareModule(osType, "name1", "version1", "description1", "vendor1"); + final SoftwareModule jvm = new SoftwareModule(runtimeType, "name2", "version1", "description1", "vendor1"); + final SoftwareModule ah = new SoftwareModule(appType, "name3", "version1", "description1", "vendor1"); + + final List modules = new ArrayList<>(); + modules.add(os); + modules.add(jvm); + modules.add(ah); + + final long current = System.currentTimeMillis(); + + final MvcResult mvcResult = mvc + .perform(post("/rest/v1/softwaremodules/").content(JsonBuilder.softwareModules(modules)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("[0].name", equalTo("name1"))) + .andExpect(jsonPath("[0].version", equalTo("version1"))) + .andExpect(jsonPath("[0].description", equalTo("description1"))) + .andExpect(jsonPath("[0].vendor", equalTo("vendor1"))).andExpect(jsonPath("[0].type", equalTo("os"))) + .andExpect(jsonPath("[0].createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("[1].name", equalTo("name2"))) + .andExpect(jsonPath("[1].version", equalTo("version1"))) + .andExpect(jsonPath("[1].description", equalTo("description1"))) + .andExpect(jsonPath("[1].vendor", equalTo("vendor1"))) + .andExpect(jsonPath("[1].type", equalTo("runtime"))) + .andExpect(jsonPath("[1].createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("[2].name", equalTo("name3"))) + .andExpect(jsonPath("[2].version", equalTo("version1"))) + .andExpect(jsonPath("[2].description", equalTo("description1"))) + .andExpect(jsonPath("[2].vendor", equalTo("vendor1"))) + .andExpect(jsonPath("[2].type", equalTo("application"))) + .andExpect(jsonPath("[2].createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("[2].createdAt", not(equalTo(0)))).andReturn(); + + final SoftwareModule osCreated = softwareManagement.findSoftwareModuleByNameAndVersion("name1", "version1") + .get(0); + final SoftwareModule jvmCreated = softwareManagement.findSoftwareModuleByNameAndVersion("name2", "version1") + .get(0); + final SoftwareModule ahCreated = softwareManagement.findSoftwareModuleByNameAndVersion("name3", "version1") + .get(0); + + assertThat( + JsonPath.compile("[0]_links.self.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/softwaremodules/" + osCreated.getId()); + assertThat(JsonPath.compile("[0]_links.artifacts.href").read(mvcResult.getResponse().getContentAsString()) + .toString()).isEqualTo("http://localhost/rest/v1/softwaremodules/" + osCreated.getId() + "/artifacts"); + + assertThat( + JsonPath.compile("[1]_links.self.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/softwaremodules/" + jvmCreated.getId()); + assertThat(JsonPath.compile("[1]_links.artifacts.href").read(mvcResult.getResponse().getContentAsString()) + .toString()).isEqualTo("http://localhost/rest/v1/softwaremodules/" + jvmCreated.getId() + "/artifacts"); + + assertThat( + JsonPath.compile("[2]_links.self.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/softwaremodules/" + ahCreated.getId()); + assertThat(JsonPath.compile("[2]_links.artifacts.href").read(mvcResult.getResponse().getContentAsString()) + .toString()).isEqualTo("http://localhost/rest/v1/softwaremodules/" + ahCreated.getId() + "/artifacts"); + + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(3); + assertThat(softwareManagement.findSoftwareModulesByType(pageReq, osType).getContent().get(0).getName()) + .isEqualTo(os.getName()); + assertThat(softwareManagement.findSoftwareModulesByType(pageReq, osType).getContent().get(0).getCreatedBy()) + .isEqualTo("uploadTester"); + assertThat(softwareManagement.findSoftwareModulesByType(pageReq, osType).getContent().get(0).getCreatedAt()) + .isGreaterThanOrEqualTo(current); + assertThat(softwareManagement.findSoftwareModulesByType(pageReq, runtimeType).getContent().get(0).getName()) + .isEqualTo(jvm.getName()); + assertThat(softwareManagement.findSoftwareModulesByType(pageReq, appType).getContent().get(0).getName()) + .isEqualTo(ah.getName()); + } + + @Test + public void deleteUnassignedSoftwareModule() throws Exception { + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).isEmpty(); + assertThat(artifactRepository.findAll()).isEmpty(); + + SoftwareModule sm = new SoftwareModule(osType, "name 1", "version 1", null, null); + sm = softwareManagement.createSoftwareModule(sm); + + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + + final Artifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), sm.getId(), + "file1", false); + + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(1); + assertThat(artifactRepository.findAll()).hasSize(1); + assertThat(softwareModuleRepository.findAll()).hasSize(1); + + mvc.perform(delete("/rest/v1/softwaremodules/{smId}", sm.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).isEmpty(); + assertThat(softwareModuleRepository.findAll()).isEmpty(); + assertThat(artifactRepository.findAll()).isEmpty(); + } + + @Test + public void deleteAssignedSoftwareModule() throws Exception { + // check baseline + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).isEmpty(); + assertThat(artifactRepository.findAll()).isEmpty(); + + final DistributionSet ds1 = TestDataUtil.generateDistributionSet("a", softwareManagement, + distributionSetManagement); + + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + + final LocalArtifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + ds1.findFirstModuleByType(appType).getId(), "file1", false); + + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(3); + assertThat(artifactRepository.findAll()).hasSize(1); + assertThat(softwareModuleRepository.findAll()).hasSize(3); + + mvc.perform(delete("/rest/v1/softwaremodules/{smId}", ds1.findFirstModuleByType(appType).getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + mvc.perform(delete("/rest/v1/softwaremodules/{smId}", ds1.findFirstModuleByType(runtimeType).getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + mvc.perform(delete("/rest/v1/softwaremodules/{smId}", ds1.findFirstModuleByType(osType).getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + // all 3 are now marked as deleted + assertThat(softwareManagement.findSoftwareModulesAll(pageReq).getNumber()).isEqualTo(0); + assertThat(softwareModuleRepository.findAll()).hasSize(3); + assertThat(artifactRepository.findAll()).hasSize(1); + } + + @Test + public void testDeleteArtifact() throws Exception { + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).isEmpty(); + assertThat(artifactRepository.findAll()).isEmpty(); + + // Create 1 SM + SoftwareModule sm = new SoftwareModule(osType, "name 1", "version 1", null, null); + sm = softwareManagement.createSoftwareModule(sm); + + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + + // Create 2 artifacts + final LocalArtifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + sm.getId(), "file1", false); + final LocalArtifact artifact2 = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + sm.getId(), "file2", false); + + // check repo before delete + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(1); + + assertThat(softwareManagement.findSoftwareModuleWithDetails(sm.getId()).getArtifacts()).hasSize(2); + assertThat(artifactRepository.findAll()).hasSize(2); + + // delete + mvc.perform(delete("/rest/v1/softwaremodules/{smId}/artifacts/{artId}", sm.getId(), artifact.getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + // check that only one artifact is still alive and still assigned + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(1); + assertThat(artifactRepository.findAll()).hasSize(1); + assertThat(softwareManagement.findSoftwareModuleWithDetails(sm.getId()).getArtifacts()).hasSize(1); + + } + + @Test + public void createMetadata() throws Exception { + + final String knownKey1 = "knownKey1"; + final String knownValue1 = "knownValue1"; + final String knownKey2 = "knownKey1"; + final String knownValue2 = "knownValue1"; + + final SoftwareModule sm = softwareManagement + .createSoftwareModule(new SoftwareModule(osType, "name 1", "version 1", null, null)); + + final JSONArray jsonArray = new JSONArray(); + jsonArray.put(new JSONObject().put("key", knownKey1).put("value", knownValue1)); + jsonArray.put(new JSONObject().put("key", knownKey2).put("value", knownValue2)); + + mvc.perform(post("/rest/v1/softwaremodules/{swId}/metadata", sm.getId()).contentType(MediaType.APPLICATION_JSON) + .content(jsonArray.toString())).andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("[0]key", equalTo(knownKey1))).andExpect(jsonPath("[0]value", equalTo(knownValue1))) + .andExpect(jsonPath("[1]key", equalTo(knownKey2))) + .andExpect(jsonPath("[1]value", equalTo(knownValue2))); + + final SoftwareModuleMetadata metaKey1 = softwareManagement.findOne(new SwMetadataCompositeKey(sm, knownKey1)); + final SoftwareModuleMetadata metaKey2 = softwareManagement.findOne(new SwMetadataCompositeKey(sm, knownKey2)); + + assertThat(metaKey1.getValue()).isEqualTo(knownValue1); + assertThat(metaKey2.getValue()).isEqualTo(knownValue2); + } + + @Test + public void updateMetadata() throws Exception { + // prepare and create metadata for update + final String knownKey = "knownKey"; + final String knownValue = "knownValue"; + final String updateValue = "valueForUpdate"; + + final SoftwareModule sm = softwareManagement + .createSoftwareModule(new SoftwareModule(osType, "name 1", "version 1", null, null)); + softwareManagement.createSoftwareModuleMetadata(new SoftwareModuleMetadata(knownKey, sm, knownValue)); + + final JSONObject jsonObject = new JSONObject().put("key", knownKey).put("value", updateValue); + + mvc.perform(put("/rest/v1/softwaremodules/{swId}/metadata/{key}", sm.getId(), knownKey) + .contentType(MediaType.APPLICATION_JSON).content(jsonObject.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("key", equalTo(knownKey))).andExpect(jsonPath("value", equalTo(updateValue))); + + final SoftwareModuleMetadata assertDS = softwareManagement.findOne(new SwMetadataCompositeKey(sm, knownKey)); + assertThat(assertDS.getValue()).isEqualTo(updateValue); + } + + @Test + public void deleteMetadata() throws Exception { + // prepare and create metadata for deletion + final String knownKey = "knownKey"; + final String knownValue = "knownValue"; + + final SoftwareModule sm = softwareManagement + .createSoftwareModule(new SoftwareModule(osType, "name 1", "version 1", null, null)); + softwareManagement.createSoftwareModuleMetadata(new SoftwareModuleMetadata(knownKey, sm, knownValue)); + + mvc.perform(delete("/rest/v1/softwaremodules/{swId}/metadata/{key}", sm.getId(), knownKey)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + try { + softwareManagement.findOne(new SwMetadataCompositeKey(sm, knownKey)); + fail("expected EntityNotFoundException but didn't throw"); + } catch (final EntityNotFoundException e) { + // ok as expected + } + } + + @Test + public void searchSoftwareModuleMetadataRsql() throws Exception { + final int totalMetadata = 10; + final String knownKeyPrefix = "knownKey"; + final String knownValuePrefix = "knownValue"; + final SoftwareModule sm = softwareManagement + .createSoftwareModule(new SoftwareModule(osType, "name 1", "version 1", null, null)); + + for (int index = 0; index < totalMetadata; index++) { + softwareManagement.createSoftwareModuleMetadata(new SoftwareModuleMetadata(knownKeyPrefix + index, + softwareManagement.findSoftwareModuleById(sm.getId()), knownValuePrefix + index)); + } + + final String rsqlSearchValue1 = "value==knownValue1"; + + mvc.perform(get("/rest/v1/softwaremodules/{swId}/metadata?q=" + rsqlSearchValue1, sm.getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("size", equalTo(1))) + .andExpect(jsonPath("total", equalTo(1))).andExpect(jsonPath("content[0].key", equalTo("knownKey1"))) + .andExpect(jsonPath("content[0].value", equalTo("knownValue1"))); + } + + private void createSoftwareModulesAlphabetical(final int amount) { + char character = 'a'; + for (int index = 0; index < amount; index++) { + final String str = String.valueOf(character); + final SoftwareModule softwareModule = new SoftwareModule(osType, str, str, str, str); + + softwareManagement.createSoftwareModule(softwareModule); + character++; + } + } +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleTypeResourceTest.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleTypeResourceTest.java new file mode 100755 index 000000000..c6aedddd4 --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/SoftwareModuleTypeResourceTest.java @@ -0,0 +1,320 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.hasSize; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.MockMvcResultPrinter; +import org.eclipse.hawkbit.WithUser; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; + +import com.jayway.jsonpath.JsonPath; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * Test for {@link SoftwareModuleTypeResource}. + * + * + * + * + */ +@Features("Component Tests - Management RESTful API") +@Stories("Software Module Type Resource") +public class SoftwareModuleTypeResourceTest extends AbstractIntegrationTest { + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/softwaremoduletypes GET requests.") + public void getSoftwareModuleTypes() throws Exception { + SoftwareModuleType testType = softwareManagement + .createSoftwareModuleType(new SoftwareModuleType("test123", "TestName123", "Desc123", 5)); + testType.setDescription("Desc1234"); + testType = softwareManagement.updateSoftwareModuleType(testType); + + mvc.perform(get("/rest/v1/softwaremoduletypes").accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$content.[?(@.key==" + osType.getKey() + ")][0].name", equalTo(osType.getName()))) + .andExpect(jsonPath("$content.[?(@.key==" + osType.getKey() + ")][0].description", + equalTo(osType.getDescription()))) + .andExpect(jsonPath("$content.[?(@.key==" + osType.getKey() + ")][0].maxAssignments", equalTo(1))) + .andExpect(jsonPath("$content.[?(@.key==" + osType.getKey() + ")][0].key", equalTo("os"))) + .andExpect(jsonPath("$content.[?(@.key==" + runtimeType.getKey() + ")][0].name", + equalTo(runtimeType.getName()))) + .andExpect(jsonPath("$content.[?(@.key==" + runtimeType.getKey() + ")][0].description", + equalTo(runtimeType.getDescription()))) + .andExpect(jsonPath("$content.[?(@.key==" + runtimeType.getKey() + ")][0].maxAssignments", equalTo(1))) + .andExpect(jsonPath("$content.[?(@.key==" + runtimeType.getKey() + ")][0].key", equalTo("runtime"))) + .andExpect( + jsonPath("$content.[?(@.key==" + appType.getKey() + ")][0].name", equalTo(appType.getName()))) + .andExpect(jsonPath("$content.[?(@.key==" + appType.getKey() + ")][0].description", + equalTo(appType.getDescription()))) + .andExpect(jsonPath("$content.[?(@.key==" + appType.getKey() + ")][0].maxAssignments", equalTo(1))) + .andExpect(jsonPath("$content.[?(@.key==" + appType.getKey() + ")][0].key", equalTo("application"))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0].id", equalTo(testType.getId().intValue()))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0].name", equalTo("TestName123"))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0].description", equalTo("Desc1234"))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0].createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0].createdAt", equalTo(testType.getCreatedAt()))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0].lastModifiedBy", equalTo("uploadTester"))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0].lastModifiedAt", + equalTo(testType.getLastModifiedAt()))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0].maxAssignments", equalTo(5))) + .andExpect(jsonPath("$content.[?(@.key==test123)][0].key", equalTo("test123"))) + .andExpect(jsonPath("$total", equalTo(4))); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/softwaremoduletypes POST requests.") + public void createSoftwareModuleTypes() throws JSONException, Exception { + + final List types = new ArrayList<>(); + types.add(new SoftwareModuleType("test1", "TestName1", "Desc1", 1)); + types.add(new SoftwareModuleType("test2", "TestName2", "Desc2", 2)); + types.add(new SoftwareModuleType("test3", "TestName3", "Desc3", 3)); + + final MvcResult mvcResult = mvc + .perform(post("/rest/v1/softwaremoduletypes/").content(JsonBuilder.softwareModuleTypes(types)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("[0].name", equalTo("TestName1"))).andExpect(jsonPath("[0].key", equalTo("test1"))) + .andExpect(jsonPath("[0].description", equalTo("Desc1"))) + .andExpect(jsonPath("[0].createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("[0].maxAssignments", equalTo(1))) + .andExpect(jsonPath("[1].name", equalTo("TestName2"))).andExpect(jsonPath("[1].key", equalTo("test2"))) + .andExpect(jsonPath("[1].description", equalTo("Desc2"))) + .andExpect(jsonPath("[1].createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("[1].maxAssignments", equalTo(2))) + .andExpect(jsonPath("[2].name", equalTo("TestName3"))).andExpect(jsonPath("[2].key", equalTo("test3"))) + .andExpect(jsonPath("[2].description", equalTo("Desc3"))) + .andExpect(jsonPath("[2].createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("[2].createdAt", not(equalTo(0)))) + .andExpect(jsonPath("[2].maxAssignments", equalTo(3))).andReturn(); + + final SoftwareModuleType created1 = softwareManagement.findSoftwareModuleTypeByKey("test1"); + final SoftwareModuleType created2 = softwareManagement.findSoftwareModuleTypeByKey("test2"); + final SoftwareModuleType created3 = softwareManagement.findSoftwareModuleTypeByKey("test3"); + + assertThat( + JsonPath.compile("[0]_links.self.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/softwaremoduletypes/" + created1.getId()); + assertThat( + JsonPath.compile("[1]_links.self.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/softwaremoduletypes/" + created2.getId()); + assertThat( + JsonPath.compile("[2]_links.self.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/softwaremoduletypes/" + created3.getId()); + + assertThat(softwareManagement.countSoftwareModuleTypesAll()).isEqualTo(6); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/softwaremoduletypes/{ID} GET requests.") + public void getSoftwareModuleType() throws Exception { + SoftwareModuleType testType = softwareManagement + .createSoftwareModuleType(new SoftwareModuleType("test123", "TestName123", "Desc123", 5)); + testType.setDescription("Desc1234"); + testType = softwareManagement.updateSoftwareModuleType(testType); + + mvc.perform(get("/rest/v1/softwaremoduletypes/{smtId}", testType.getId()).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$name", equalTo("TestName123"))) + .andExpect(jsonPath("$description", equalTo("Desc1234"))) + .andExpect(jsonPath("$maxAssignments", equalTo(5))) + .andExpect(jsonPath("$createdBy", equalTo("uploadTester"))) + .andExpect(jsonPath("$createdAt", equalTo(testType.getCreatedAt()))) + .andExpect(jsonPath("$lastModifiedBy", equalTo("uploadTester"))) + .andExpect(jsonPath("$lastModifiedAt", equalTo(testType.getLastModifiedAt()))); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/softwaremoduletypes/{ID} DELETE requests (hard delete scenario).") + public void deleteSoftwareModuleTypeUnused() throws Exception { + final SoftwareModuleType testType = softwareManagement + .createSoftwareModuleType(new SoftwareModuleType("test123", "TestName123", "Desc123", 5)); + + assertThat(softwareManagement.countSoftwareModuleTypesAll()).isEqualTo(4); + + mvc.perform(delete("/rest/v1/softwaremoduletypes/{smId}", testType.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + assertThat(softwareManagement.countSoftwareModuleTypesAll()).isEqualTo(3); + } + + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/softwaremoduletypes/{ID} DELETE requests (soft delete scenario).") + public void deleteSoftwareModuleTypeUsed() throws Exception { + final SoftwareModuleType testType = softwareManagement + .createSoftwareModuleType(new SoftwareModuleType("test123", "TestName123", "Desc123", 5)); + softwareManagement + .createSoftwareModule(new SoftwareModule(testType, "name", "version", "description", "vendor")); + + assertThat(softwareManagement.countSoftwareModuleTypesAll()).isEqualTo(4); + assertThat(softwareModuleTypeRepository.count()).isEqualTo(4); + + mvc.perform(delete("/rest/v1/softwaremoduletypes/{smId}", testType.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + assertThat(softwareModuleTypeRepository.count()).isEqualTo(4); + assertThat(softwareManagement.countSoftwareModuleTypesAll()).isEqualTo(3); + } + + @Test + @Description("Checks the correct behaviour of /rest/v1/softwaremoduletypes/{ID} PUT requests.") + public void updateSoftwareModuleTypeOnlyDescriptionAndNameUntouched() throws Exception { + final SoftwareModuleType testType = softwareManagement + .createSoftwareModuleType(new SoftwareModuleType("test123", "TestName123", "Desc123", 5)); + + final String body = new JSONObject().put("id", testType.getId()).put("description", "foobardesc") + .put("name", "nameShouldNotBeChanged").toString(); + + mvc.perform(put("/rest/v1/softwaremoduletypes/{smId}", testType.getId()).content(body) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$id", equalTo(testType.getId().intValue()))) + .andExpect(jsonPath("$description", equalTo("foobardesc"))) + .andExpect(jsonPath("$name", equalTo("TestName123"))).andReturn(); + + } + + @Test + @Description("Checks the correct behaviour of /rest/v1/softwaremoduletypes GET requests with paging.") + public void getSoftwareModuleTypesWithoutAddtionalRequestParameters() throws Exception { + final int types = 3; + mvc.perform(get(RestConstants.SOFTWAREMODULETYPE_V1_REQUEST_MAPPING)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_TOTAL, equalTo(types))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_SIZE, equalTo(types))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_CONTENT, hasSize(types))); + } + + @Test + @Description("Checks the correct behaviour of /rest/v1/softwaremoduletypes GET requests with paging.") + public void getSoftwareModuleTypesWithPagingLimitRequestParameter() throws Exception { + final int types = 3; + final int limitSize = 1; + mvc.perform(get(RestConstants.SOFTWAREMODULETYPE_V1_REQUEST_MAPPING) + .param(RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(limitSize))) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_TOTAL, equalTo(types))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_SIZE, equalTo(limitSize))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_CONTENT, hasSize(limitSize))); + } + + @Test + @Description("Checks the correct behaviour of /rest/v1/softwaremoduletypes GET requests with paging.") + public void getSoftwareModuleTypesWithPagingLimitAndOffsetRequestParameter() throws Exception { + final int types = 3; + final int offsetParam = 2; + final int expectedSize = types - offsetParam; + mvc.perform(get(RestConstants.SOFTWAREMODULETYPE_V1_REQUEST_MAPPING) + .param(RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, String.valueOf(offsetParam)) + .param(RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(types))) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_TOTAL, equalTo(types))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_SIZE, equalTo(expectedSize))) + .andExpect(jsonPath(TargetResourceTest.JSON_PATH_PAGED_LIST_CONTENT, hasSize(expectedSize))); + } + + @Test + @Description("Ensures that the server is behaving as expected on invalid requests (wrong media type, wrong ID etc.).") + public void invalidRequestsOnSoftwaremoduleTypesResource() throws Exception { + final SoftwareModuleType testType = softwareManagement + .createSoftwareModuleType(new SoftwareModuleType("test123", "TestName123", "Desc123", 5)); + + final List types = new ArrayList<>(); + types.add(testType); + + // SM does not exist + mvc.perform(get("/rest/v1/softwaremoduletypes/12345678")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + mvc.perform(delete("/rest/v1/softwaremoduletypes/12345678")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + // bad request - no content + mvc.perform(post("/rest/v1/softwaremoduletypes").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()); + + // bad request - bad content + mvc.perform(post("/rest/v1/softwaremoduletypes").content("sdfjsdlkjfskdjf".getBytes()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()); + + // unsupported media type + mvc.perform(post("/rest/v1/softwaremoduletypes").content(JsonBuilder.softwareModuleTypes(types)) + .contentType(MediaType.APPLICATION_OCTET_STREAM)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isUnsupportedMediaType()); + + // not allowed methods + mvc.perform(put("/rest/v1/softwaremoduletypes")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete("/rest/v1/softwaremoduletypes")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + } + + @Test + @Description("Search erquest of software module types.") + public void searchSoftwareModuleTypeRsql() throws Exception { + final SoftwareModuleType testType = softwareManagement + .createSoftwareModuleType(new SoftwareModuleType("test123", "TestName123", "Desc123", 5)); + final SoftwareModuleType testType2 = softwareManagement + .createSoftwareModuleType(new SoftwareModuleType("test1234", "TestName1234", "Desc123", 5)); + + final String rsqlFindLikeDs1OrDs2 = "name==TestName123,name==TestName1234"; + + mvc.perform(get("/rest/v1/softwaremoduletypes?q=" + rsqlFindLikeDs1OrDs2)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("size", equalTo(2))) + .andExpect(jsonPath("total", equalTo(2))).andExpect(jsonPath("content[0].name", equalTo("TestName123"))) + .andExpect(jsonPath("content[1].name", equalTo("TestName1234"))); + + } + + private void createSoftwareModulesAlphabetical(final int amount) { + char character = 'a'; + for (int index = 0; index < amount; index++) { + final String str = String.valueOf(character); + final SoftwareModule softwareModule = new SoftwareModule(osType, str, str, str, str); + + softwareManagement.createSoftwareModule(softwareModule); + character++; + } + } + +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/SortUtilityTest.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/SortUtilityTest.java new file mode 100755 index 000000000..7ca7814b2 --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/SortUtilityTest.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.eclipse.hawkbit.repository.TargetFields; +import org.junit.Test; +import org.springframework.data.domain.Sort.Order; + +/** + * + */ +public class SortUtilityTest { + private static final String SORT_PARAM_1 = "NAME:ASC"; + private static final String SORT_PARAM_2 = "NAME:ASC, DESCRIPTION:DESC"; + private static final String SYNTAX_FAILURE_SORT_PARAM = "NAME:ASC DESCRIPTION:DESC"; + private static final String WRONG_DIRECTION_PARAM = "NAME:ASDF"; + private static final String CASE_INSENSITIVE_DIRECTION_PARAM = "NaME:ASC"; + private static final String CASE_INSENSITIVE_DIRECTION_PARAM_1 = "name:ASC"; + private static final String WRONG_FIELD_PARAM = "ASDF:ASC"; + + @Test + public void parseSortParam1() { + + final List parse = SortUtility.parse(TargetFields.class, SORT_PARAM_1); + assertEquals(1, parse.size()); + } + + @Test + public void parseSortParam2() { + final List parse = SortUtility.parse(TargetFields.class, SORT_PARAM_2); + assertEquals(2, parse.size()); + } + + @Test(expected = SortParameterSyntaxErrorException.class) + public void parseWrongSyntaxParam() { + SortUtility.parse(TargetFields.class, SYNTAX_FAILURE_SORT_PARAM); + } + + @Test + public void parsingIsNotCaseSensitive() { + SortUtility.parse(TargetFields.class, CASE_INSENSITIVE_DIRECTION_PARAM); + SortUtility.parse(TargetFields.class, CASE_INSENSITIVE_DIRECTION_PARAM_1); + } + + @Test(expected = SortParameterUnsupportedDirectionException.class) + public void parseWrongDirectionParam() { + SortUtility.parse(TargetFields.class, WRONG_DIRECTION_PARAM); + } + + @Test(expected = SortParameterUnsupportedFieldException.class) + public void parseWrongFieldParam() { + SortUtility.parse(TargetFields.class, WRONG_FIELD_PARAM); + } +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/SystemManagementResourceTest.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/SystemManagementResourceTest.java new file mode 100755 index 000000000..4c94d03a7 --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/SystemManagementResourceTest.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.MockMvcResultPrinter; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.WithUser; +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.junit.Test; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * + * + */ +@Features("Component Tests - System Management RESTful API") +@Stories("System Management Resource") +public class SystemManagementResourceTest extends AbstractIntegrationTest { + + @Test + @WithUser(tenantId = "mytenant", allSpPermissions = true) + @Description("Tests that a tenant can be deletd by API.") + public void deleteTenant() throws Exception { + + final DistributionSet dsA = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + + assertThat(distributionSetManagement.findDistributionSetById(dsA.getId())).isNotNull(); + + mvc.perform(delete("/system/admin/tenants/mytenant")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + assertThat(distributionSetManagement.findDistributionSetById(dsA.getId())).isNull(); + + } + + @Test + @WithUser(tenantId = "mytenant", authorities = { SpPermission.DELETE_TARGET, SpPermission.DELETE_REPOSITORY, + SpPermission.CREATE_REPOSITORY, SpPermission.READ_REPOSITORY }) + @Description("Tenant deletion is only possible for SYSTEM_ADMINs. Repository or target delete is not sufficient.") + public void deleteTenantFailsMissingPermission() throws Exception { + + final DistributionSet dsA = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + assertThat(distributionSetManagement.findDistributionSetById(dsA.getId())).isNotNull(); + + mvc.perform(delete("/system/admin/tenants/mytenant")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isForbidden()); + + assertThat(distributionSetManagement.findDistributionSetById(dsA.getId())).isNotNull(); + } + + @Test + public void getCachesReturnStatus200() throws Exception { + mvc.perform(get("/system/admin/caches")).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + } + + @Test + public void invalidateCachesReturnStatus200() throws Exception { + mvc.perform(delete("/system/admin/caches")).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + } + +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/TargetResourceTest.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/TargetResourceTest.java new file mode 100755 index 000000000..0089769d0 --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/TargetResourceTest.java @@ -0,0 +1,1209 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.hasSize; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.AbstractIntegrationTest; +import org.eclipse.hawkbit.MockMvcResultPrinter; +import org.eclipse.hawkbit.TestDataUtil; +import org.eclipse.hawkbit.WithUser; +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.repository.ActionStatusFields; +import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionStatus; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetInfo; +import org.eclipse.hawkbit.rest.resource.model.ExceptionInfo; +import org.eclipse.hawkbit.util.IpUtil; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; + +import com.google.common.collect.Lists; +import com.jayway.jsonpath.JsonPath; + +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +/** + * + * Spring MVC Tests against the TargetResource. + * + * + * + */ +@Features("Component Tests - Management RESTful API") +@Stories("Target Resource") +// TODO: fully document tests -> @Description for long text and reasonable +// method name as short text +public class TargetResourceTest extends AbstractIntegrationTest { + + private static final String TARGET_DESCRIPTION_TEST = "created in test"; + + // json paths + private static final String JSON_PATH_ROOT = "$"; + + // fields, attributes + private static final String JSON_PATH_FIELD_ID = ".id"; + private static final String JSON_PATH_FIELD_CONTROLLERID = ".controllerId"; + private static final String JSON_PATH_FIELD_NAME = ".name"; + private static final String JSON_PATH_FIELD_DESCRIPTION = ".description"; + private static final String JSON_PATH_FIELD_CONTENT = ".content"; + private static final String JSON_PATH_FIELD_SIZE = ".size"; + private static final String JSON_PATH_FIELD_TOTAL = ".total"; + + // target + // $.field + static final String JSON_PATH_PAGED_LIST_CONTENT = JSON_PATH_ROOT + JSON_PATH_FIELD_CONTENT; + static final String JSON_PATH_PAGED_LIST_SIZE = JSON_PATH_ROOT + JSON_PATH_FIELD_SIZE; + static final String JSON_PATH_PAGED_LIST_TOTAL = JSON_PATH_ROOT + JSON_PATH_FIELD_TOTAL; + + private static final String JSON_PATH_NAME = JSON_PATH_ROOT + JSON_PATH_FIELD_NAME; + private static final String JSON_PATH_ID = JSON_PATH_ROOT + JSON_PATH_FIELD_ID; + private static final String JSON_PATH_CONTROLLERID = JSON_PATH_ROOT + JSON_PATH_FIELD_CONTROLLERID; + private static final String JSON_PATH_DESCRIPTION = JSON_PATH_ROOT + JSON_PATH_FIELD_DESCRIPTION; + + // TODO kzimmerm: test *modified after entity change + + @Test + // MECS-1064 + public void getActionStatusReturnsCorrectType() throws Exception { + final int limitSize = 2; + final String knownTargetId = "targetId"; + final List actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId); + actions.get(0).setStatus(Status.FINISHED); + controllerManagament.addUpdateActionStatus( + new ActionStatus(actions.get(0), Status.FINISHED, System.currentTimeMillis(), "testmessage"), + actions.get(0)); + + final PageRequest pageRequest = new PageRequest(0, 1000, Direction.ASC, ActionStatusFields.ID.getFieldName()); + + // limit to 1 - first page -> standard cancel message + final Long reportAt = deploymentManagement + .findActionsByTarget(pageRequest, targetManagement.findTargetByControllerID(knownTargetId)).getContent() + .get(0).getCreatedAt(); + final Long id = deploymentManagement + .findActionsByTarget(pageRequest, targetManagement.findTargetByControllerID(knownTargetId)).getContent() + .get(0).getId(); + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/" + actions.get(0).getId() + "/status") + .param(RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(limitSize)) + .param(RestConstants.REQUEST_PARAMETER_SORTING, "ID:ASC")) + .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(3))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(limitSize))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(limitSize))) + .andExpect(jsonPath("content.[0].id", equalTo(id.intValue()))) + .andExpect(jsonPath("content.[0].type", equalTo("finished"))) + .andExpect(jsonPath("content.[0].messages", hasSize(1))) + .andExpect(jsonPath("content.[0].reportedAt", equalTo(reportAt))) + .andExpect(jsonPath("content.[1].type", equalTo("canceling"))); + } + + @Test + @WithUser(allSpPermissions = false, authorities = { SpPermission.READ_TARGET, SpPermission.CREATE_TARGET }) + public void securityTokenIsNotInResponseIfMissingPermission() throws Exception { + + final String knownControllerId = "knownControllerId"; + targetManagement.createTarget(new Target(knownControllerId)); + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}", knownControllerId)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("securityToken").doesNotExist()); + } + + @Test + @WithUser(allSpPermissions = false, authorities = { SpPermission.READ_TARGET, SpPermission.CREATE_TARGET, + SpPermission.READ_TARGET_SEC_TOKEN }) + public void securityTokenIsInResponseWithCorrectPermission() throws Exception { + + final String knownControllerId = "knownControllerId"; + final Target createTarget = targetManagement.createTarget(new Target(knownControllerId)); + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}", knownControllerId)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("securityToken", equalTo(createTarget.getSecurityToken()))); + } + + @Test + public void addressAndIpAddressInTargetResult() throws Exception { + // prepare targets with IP + final String knownControllerId1 = "0815"; + final String knownControllerId2 = "4711"; + + createTarget(knownControllerId1); + createTarget(knownControllerId2); + + // test + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(2))) + .andExpect(jsonPath("size", equalTo(2))) + .andExpect(jsonPath("$content.[?(@.controllerId==" + knownControllerId1 + ")][0].ipAddress", + equalTo("127.0.0.1"))) + .andExpect(jsonPath("$content.[?(@.controllerId==" + knownControllerId2 + ")][0].ipAddress", + equalTo("127.0.0.1"))) + .andExpect(jsonPath("$content.[?(@.controllerId==" + knownControllerId1 + ")][0].address", + equalTo(IpUtil.createHttpUri("127.0.0.1").toString()))) + .andExpect(jsonPath("$content.[?(@.controllerId==" + knownControllerId2 + ")][0].address", + equalTo(IpUtil.createHttpUri("127.0.0.1").toString()))); + } + + private void createTarget(final String controllerId) { + final Target target = new Target(controllerId); + final TargetInfo targetInfo = new TargetInfo(target); + targetInfo.setAddress(IpUtil.createHttpUri("127.0.0.1").toString()); + target.setTargetInfo(targetInfo); + targetManagement.createTarget(target); + } + + @Test + public void searchActionsRsql() throws Exception { + + // prepare test + final DistributionSet dsA = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + final Target createTarget = targetManagement.createTarget(new Target("knownTargetId")); + + deploymentManagement.assignDistributionSet(dsA, Lists.newArrayList(createTarget)); + + final String rsqlPendingStatus = "status==pending"; + final String rsqlFinishedStatus = "status==finished"; + final String rsqlPendingOrFinishedStatus = rsqlFinishedStatus + "," + rsqlPendingStatus; + // pending status one result + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/actions?q=" + rsqlPendingStatus, + createTarget.getControllerId())).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("total", equalTo(1))).andExpect(jsonPath("size", equalTo(1))) + .andExpect(jsonPath("content[0].status", equalTo("pending"))); + + // finished status none result + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/actions?q=" + rsqlFinishedStatus, + createTarget.getControllerId())).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("total", equalTo(0))).andExpect(jsonPath("size", equalTo(0))); + + // pending or finished status one result + mvc.perform( + get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/actions?q=" + rsqlPendingOrFinishedStatus, + createTarget.getControllerId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("total", equalTo(1))) + .andExpect(jsonPath("size", equalTo(1))).andExpect(jsonPath("content[0].status", equalTo("pending"))); + } + + @Test + public void cancelActionOK() throws Exception { + // prepare test + final DistributionSet dsA = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + ; + final Target tA = targetManagement + .createTarget(TestDataUtil.buildTargetFixture("target-id-A", "first description")); + // assign a distribution set so we get an active update action + deploymentManagement.assignDistributionSet(dsA, Lists.newArrayList(tA)); + + // verify active action + final Slice actionsByTarget = deploymentManagement.findActionsByTarget(new PageRequest(0, 100), tA); + assertThat(actionsByTarget.getContent()).hasSize(1); + + // test - cancel the active action + mvc.perform(delete(RestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/actions/{actionId}", + tA.getControllerId(), actionsByTarget.getContent().get(0).getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNoContent()); + + final Action action = deploymentManagement.findAction(actionsByTarget.getContent().get(0).getId()); + // still active because in "canceling" state and waiting for controller + // feedback + assertThat(action.isActive()).isTrue(); + + final Target queryTarget = targetManagement.findTargetByControllerID(tA.getControllerId()); + // action has not been cancelled confirmed from controller, so DS + // remains assigned until + // confirmation + assertThat(queryTarget.getAssignedDistributionSet()).isNotNull(); + assertThat(queryTarget.getTargetInfo().getInstalledDistributionSet()).isNull(); + } + + @Test + public void cancelAnCancelActionIsNotAllowed() throws Exception { + // prepare test + final DistributionSet dsA = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + ; + Target tA = targetManagement.createTarget(TestDataUtil.buildTargetFixture("target-id-A", "first description")); + // assign a distribution set so we get an active update action + deploymentManagement.assignDistributionSet(dsA, Lists.newArrayList(tA)); + // verify active action + final Slice actionsByTarget = deploymentManagement.findActionsByTarget(new PageRequest(0, 100), tA); + assertThat(actionsByTarget.getContent()).hasSize(1); + // cancel the active action + tA = targetManagement.findTargetByControllerID(tA.getControllerId()); + deploymentManagement.cancelAction(actionsByTarget.getContent().get(0), tA); + + // find the current active action + final List cancelActions = deploymentManagement.findActionsByTarget(new PageRequest(0, 100), tA) + .getContent().stream().filter(action -> action.isCancelingOrCanceled()).collect(Collectors.toList()); + assertThat(cancelActions).hasSize(1); + + // test - cancel an cancel action returns forbidden + mvc.perform(delete(RestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/actions/{actionId}", + tA.getControllerId(), cancelActions.get(0).getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + } + + @Test + public void deleteTargetReturnsOK() throws Exception { + final String knownControllerId = "knownControllerIdDelete"; + targetManagement.createTarget(new Target(knownControllerId)); + + mvc.perform(delete(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownControllerId)) + .andExpect(status().isOk()); + + final Target findTargetByControllerID = targetManagement.findTargetByControllerID(knownControllerId); + assertThat(findTargetByControllerID).isNull(); + } + + @Test + public void deleteTargetWhichDoesNotExistsLeadsToEntityNotFound() throws Exception { + final String knownControllerId = "knownControllerIdDelete"; + + mvc.perform(delete(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownControllerId)) + .andExpect(status().isNotFound()); + } + + @Test + public void updateTargetWhichDoesNotExistsLeadsToEntityNotFound() throws Exception { + final String knownControllerId = "knownControllerIdUpdate"; + mvc.perform(put(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownControllerId).content("{}") + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + } + + @Test + public void updateTargetDescription() throws Exception { + final String knownControllerId = "123"; + final String knownNewDescription = "a new desc updated over rest"; + final String knownNameNotModiy = "nameNotModiy"; + final String body = new JSONObject().put("description", knownNewDescription).toString(); + + // prepare + final Target t = new Target(knownControllerId); + t.setDescription("old description"); + t.setName(knownNameNotModiy); + targetManagement.createTarget(t); + + mvc.perform(put(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownControllerId).content(body) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.controllerId", equalTo(knownControllerId))) + .andExpect(jsonPath("$.description", equalTo(knownNewDescription))) + .andExpect(jsonPath("$.name", equalTo(knownNameNotModiy))); + + final Target findTargetByControllerID = targetManagement.findTargetByControllerID(knownControllerId); + assertThat(findTargetByControllerID.getDescription()).isEqualTo(knownNewDescription); + assertThat(findTargetByControllerID.getName()).isEqualTo(knownNameNotModiy); + } + + @Test + public void getTargetWithoutAddtionalRequestParameters() throws Exception { + final int knownTargetAmount = 3; + final String idA = "a"; + final String idB = "b"; + final String idC = "c"; + final String linksHrefPrefix = "http://localhost/rest/v1/targets/"; + createTargetsAlphabetical(knownTargetAmount); + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING)).andExpect(status().isOk()) + .andDo(MockMvcResultPrinter.print()) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(knownTargetAmount))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(knownTargetAmount))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(knownTargetAmount))) + // idA + .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0]._links.self.href", + equalTo(linksHrefPrefix + idA))) + .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0].name", equalTo(idA))) + .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0].description", equalTo(idA))) + .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0].controllerId", equalTo(idA))) + .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0].createdBy", equalTo("bumlux"))) + .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0].updateStatus", equalTo("unknown"))) + // idB + .andExpect(jsonPath("$content.[?(@.name==" + idB + ")][0]._links.self.href", + equalTo(linksHrefPrefix + idB))) + .andExpect(jsonPath("$content.[?(@.name==" + idB + ")][0].name", equalTo(idB))) + .andExpect(jsonPath("$content.[?(@.name==" + idB + ")][0].description", equalTo(idB))) + .andExpect(jsonPath("$content.[?(@.name==" + idB + ")][0].controllerId", equalTo(idB))) + .andExpect(jsonPath("$content.[?(@.name==" + idB + ")][0].createdBy", equalTo("bumlux"))) + .andExpect(jsonPath("$content.[?(@.name==" + idB + ")][0].updateStatus", equalTo("unknown"))) + // idC + .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0]._links.self.href", + equalTo(linksHrefPrefix + idC))) + .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0].name", equalTo(idC))) + .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0].description", equalTo(idC))) + .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0].controllerId", equalTo(idC))) + .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0].createdBy", equalTo("bumlux"))) + .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0].updateStatus", equalTo("unknown"))); + } + + @Test + public void getTargetWithPagingLimitRequestParameter() throws Exception { + final int knownTargetAmount = 3; + final int limitSize = 1; + createTargetsAlphabetical(knownTargetAmount); + final String idA = "a"; + final String linksHrefPrefix = "http://localhost/rest/v1/targets/"; + + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING).param(RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, + String.valueOf(limitSize))).andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(knownTargetAmount))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(limitSize))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(limitSize))) + // idA + .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0]._links.self.href", + equalTo(linksHrefPrefix + idA))) + .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0].name", equalTo(idA))) + .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0].description", equalTo(idA))) + .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0].controllerId", equalTo(idA))) + .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0].createdBy", equalTo("bumlux"))) + .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0].updateStatus", equalTo("unknown"))); + + } + + @Test + public void getTargetWithPagingLimitAndOffsetRequestParameter() throws Exception { + final int knownTargetAmount = 5; + final int offsetParam = 2; + final int expectedSize = knownTargetAmount - offsetParam; + final String idC = "c"; + final String idD = "d"; + final String idE = "e"; + final String linksHrefPrefix = "http://localhost/rest/v1/targets/"; + + createTargetsAlphabetical(knownTargetAmount); + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING) + .param(RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, String.valueOf(offsetParam)) + .param(RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(knownTargetAmount))) + .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(knownTargetAmount))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(expectedSize))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(expectedSize))) + // idA + .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0]._links.self.href", + equalTo(linksHrefPrefix + idC))) + .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0].name", equalTo(idC))) + .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0].description", equalTo(idC))) + .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0].controllerId", equalTo(idC))) + .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0].createdBy", equalTo("bumlux"))) + .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0].updateStatus", equalTo("unknown"))) + // idB + .andExpect(jsonPath("$content.[?(@.name==" + idD + ")][0]._links.self.href", + equalTo(linksHrefPrefix + idD))) + .andExpect(jsonPath("$content.[?(@.name==" + idD + ")][0].name", equalTo(idD))) + .andExpect(jsonPath("$content.[?(@.name==" + idD + ")][0].description", equalTo(idD))) + .andExpect(jsonPath("$content.[?(@.name==" + idD + ")][0].controllerId", equalTo(idD))) + .andExpect(jsonPath("$content.[?(@.name==" + idD + ")][0].createdBy", equalTo("bumlux"))) + .andExpect(jsonPath("$content.[?(@.name==" + idD + ")][0].updateStatus", equalTo("unknown"))) + // idC + .andExpect(jsonPath("$content.[?(@.name==" + idE + ")][0]._links.self.href", + equalTo(linksHrefPrefix + idE))) + .andExpect(jsonPath("$content.[?(@.name==" + idE + ")][0].name", equalTo(idE))) + .andExpect(jsonPath("$content.[?(@.name==" + idE + ")][0].description", equalTo(idE))) + .andExpect(jsonPath("$content.[?(@.name==" + idE + ")][0].controllerId", equalTo(idE))) + .andExpect(jsonPath("$content.[?(@.name==" + idE + ")][0].createdBy", equalTo("bumlux"))) + .andExpect(jsonPath("$content.[?(@.name==" + idE + ")][0].updateStatus", equalTo("unknown"))); + } + + @Test + public void getSingleTarget() throws Exception { + // create first a target which can be retrieved by rest interface + final String knownControllerId = "1"; + final String knownName = "someName"; + createSingleTarget(knownControllerId, knownName); + final String hrefPrefix = "http://localhost/rest/v1/targets/" + knownControllerId + "/"; + // test + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownControllerId)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath(JSON_PATH_NAME, equalTo(knownName))) + .andExpect(jsonPath(JSON_PATH_CONTROLLERID, equalTo(knownControllerId))) + .andExpect(jsonPath(JSON_PATH_DESCRIPTION, equalTo(TARGET_DESCRIPTION_TEST))) + .andExpect(jsonPath("$.pollStatus", hasKey("lastRequestAt"))) + .andExpect(jsonPath("$.pollStatus", hasKey("nextExpectedRequestAt"))) + .andExpect(jsonPath("$.pollStatus.overdue", equalTo(false))) + .andExpect(jsonPath("$_links.assignedDS.href", equalTo(hrefPrefix + "assignedDS"))) + .andExpect(jsonPath("$_links.installedDS.href", equalTo(hrefPrefix + "installedDS"))) + .andExpect(jsonPath("$_links.actions.href", + equalTo(hrefPrefix + "actions" + "?offset=0&limit=50&sort=id:DESC"))); + } + + @Test + public void getSingleTargetNoExistsResponseNotFound() throws Exception { + final String targetIdNotExists = "bubu"; + // test + + final MvcResult mvcResult = mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + targetIdNotExists)) + .andExpect(status().isNotFound()).andReturn(); + + // verify response json exception message + final ExceptionInfo exceptionInfo = ResourceUtility + .convertException(mvcResult.getResponse().getContentAsString()); + assertThat(exceptionInfo.getErrorCode()).isEqualTo(SpServerError.SP_REPO_ENTITY_NOT_EXISTS.getKey()); + } + + @Test + public void getAssignedDistributionSetOfTargetIsEmpty() throws Exception { + // create first a target which can be retrieved by rest interface + final String knownControllerId = "1"; + final String knownName = "someName"; + createSingleTarget(knownControllerId, knownName); + + // test + + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownControllerId + "/assignedDS")) + .andExpect(status().isNoContent()).andExpect(content().string("")); + + } + + @Test + public void getAssignedDistributionSetOfTarget() throws Exception { + // create first a target which can be retrieved by rest interface + final String knownControllerId = "1"; + final String knownName = "someName"; + createSingleTarget(knownControllerId, knownName); + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + deploymentManagement.assignDistributionSet(ds.getId(), knownControllerId); + + // test + + final SoftwareModule os = ds.findFirstModuleByType(osType); + final SoftwareModule jvm = ds.findFirstModuleByType(runtimeType); + final SoftwareModule bApp = ds.findFirstModuleByType(appType); + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownControllerId + "/assignedDS")) + .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) + .andExpect(jsonPath(JSON_PATH_ID, equalTo(ds.getId().intValue()))) + .andExpect(jsonPath(JSON_PATH_NAME, equalTo(ds.getName()))) + .andExpect(jsonPath(JSON_PATH_DESCRIPTION, equalTo(ds.getDescription()))) + // os + .andExpect( + jsonPath("$modules.[?(@.type==" + osType.getKey() + ")][0].id", equalTo(os.getId().intValue()))) + .andExpect(jsonPath("$modules.[?(@.type==" + osType.getKey() + ")][0].name", equalTo(os.getName()))) + .andExpect(jsonPath("$modules.[?(@.type==" + osType.getKey() + ")][0].description", + equalTo(os.getDescription()))) + .andExpect( + jsonPath("$modules.[?(@.type==" + osType.getKey() + ")][0].version", equalTo(os.getVersion()))) + .andExpect(jsonPath("$modules.[?(@.type==" + osType.getKey() + ")][0].vendor", equalTo(os.getVendor()))) + .andExpect(jsonPath("$modules.[?(@.type==" + osType.getKey() + ")][0].type", equalTo("os"))) + // jvm + .andExpect(jsonPath("$modules.[?(@.type==" + runtimeType.getKey() + ")][0].id", + equalTo(jvm.getId().intValue()))) + .andExpect( + jsonPath("$modules.[?(@.type==" + runtimeType.getKey() + ")][0].name", equalTo(jvm.getName()))) + .andExpect(jsonPath("$modules.[?(@.type==" + runtimeType.getKey() + ")][0].description", + equalTo(jvm.getDescription()))) + .andExpect(jsonPath("$modules.[?(@.type==" + runtimeType.getKey() + ")][0].version", + equalTo(jvm.getVersion()))) + .andExpect(jsonPath("$modules.[?(@.type==" + runtimeType.getKey() + ")][0].vendor", + equalTo(jvm.getVendor()))) + .andExpect(jsonPath("$modules.[?(@.type==" + runtimeType.getKey() + ")][0].type", equalTo("runtime"))) + // baseApp + .andExpect(jsonPath("$modules.[?(@.type==" + appType.getKey() + ")][0].id", + equalTo(bApp.getId().intValue()))) + .andExpect(jsonPath("$modules.[?(@.type==" + appType.getKey() + ")][0].name", equalTo(bApp.getName()))) + .andExpect(jsonPath("$modules.[?(@.type==" + appType.getKey() + ")][0].description", + equalTo(bApp.getDescription()))) + .andExpect(jsonPath("$modules.[?(@.type==" + appType.getKey() + ")][0].version", + equalTo(bApp.getVersion()))) + .andExpect( + jsonPath("$modules.[?(@.type==" + appType.getKey() + ")][0].vendor", equalTo(bApp.getVendor()))) + .andExpect(jsonPath("$modules.[?(@.type==" + appType.getKey() + ")][0].type", equalTo("application"))); + + } + + @Test + public void getInstalledDistributionSetOfTargetIsEmpty() throws Exception { + // create first a target which can be retrieved by rest interface + final String knownControllerId = "1"; + final String knownName = "someName"; + createSingleTarget(knownControllerId, knownName); + + // test + + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownControllerId + "/installedDS")) + .andExpect(status().isNoContent()).andExpect(content().string("")); + + } + + @Test + @Ignore + public void getInstalledDistributionSetOfTarget() throws JSONException, Exception { + // create first a target which can be retrieved by rest interface + final String knownControllerId = "1"; + final String knownName = "someName"; + createSingleTarget(knownControllerId, knownName); + final DistributionSet ds = TestDataUtil.generateDistributionSet("", softwareManagement, + distributionSetManagement); + // assign ds to target + deploymentManagement.assignDistributionSet(ds.getId(), knownControllerId); + // give feedback, so installedDS is in SNYC + feedbackToByInSync(knownControllerId, ds); + // test + + final SoftwareModule os = ds.findFirstModuleByType(osType); + final SoftwareModule jvm = ds.findFirstModuleByType(runtimeType); + final SoftwareModule bApp = ds.findFirstModuleByType(appType); + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownControllerId + "/installedDS")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath(JSON_PATH_ID, equalTo(ds.getId().intValue()))) + .andExpect(jsonPath(JSON_PATH_NAME, equalTo(ds.getName()))) + .andExpect(jsonPath(JSON_PATH_DESCRIPTION, equalTo(ds.getDescription()))) + // os + .andExpect( + jsonPath("$modules.[?(@.type==" + osType.getKey() + ")][0].id", equalTo(os.getId().intValue()))) + .andExpect(jsonPath("$modules.[?(@.type==" + osType.getKey() + ")][0].name", equalTo(os.getName()))) + .andExpect(jsonPath("$modules.[?(@.type==" + osType.getKey() + ")][0].description", + equalTo(os.getDescription()))) + .andExpect( + jsonPath("$modules.[?(@.type==" + osType.getKey() + ")][0].version", equalTo(os.getVersion()))) + .andExpect(jsonPath("$modules.[?(@.type==" + osType.getKey() + ")][0].vendor", equalTo(os.getVendor()))) + .andExpect(jsonPath("$modules.[?(@.type==" + osType.getKey() + ")][0].type", equalTo("os"))) + // jvm + .andExpect(jsonPath("$modules.[?(@.type==" + runtimeType.getKey() + ")][0].id", + equalTo(jvm.getId().intValue()))) + .andExpect( + jsonPath("$modules.[?(@.type==" + runtimeType.getKey() + ")][0].name", equalTo(jvm.getName()))) + .andExpect(jsonPath("$modules.[?(@.type==" + runtimeType.getKey() + ")][0].description", + equalTo(jvm.getDescription()))) + .andExpect(jsonPath("$modules.[?(@.type==" + runtimeType.getKey() + ")][0].version", + equalTo(jvm.getVersion()))) + .andExpect(jsonPath("$modules.[?(@.type==" + runtimeType.getKey() + ")][0].vendor", + equalTo(jvm.getVendor()))) + .andExpect(jsonPath("$modules.[?(@.type==" + runtimeType.getKey() + ")][0].type", equalTo("runtime"))) + // baseApp + .andExpect(jsonPath("$modules.[?(@.type==" + appType.getKey() + ")][0].id", + equalTo(bApp.getId().intValue()))) + .andExpect(jsonPath("$modules.[?(@.type==" + appType.getKey() + ")][0].name", equalTo(bApp.getName()))) + .andExpect(jsonPath("$modules.[?(@.type==" + appType.getKey() + ")][0].description", + equalTo(bApp.getDescription()))) + .andExpect(jsonPath("$modules.[?(@.type==" + appType.getKey() + ")][0].version", + equalTo(bApp.getVersion()))) + .andExpect( + jsonPath("$modules.[?(@.type==" + appType.getKey() + ")][0].vendor", equalTo(bApp.getVendor()))) + .andExpect(jsonPath("$modules.[?(@.type==" + appType.getKey() + ")][0].type", equalTo("application"))); + } + + @Test + public void createTargetWithoutPayloadBadRequest() throws Exception { + + final MvcResult mvcResult = mvc + .perform(post(RestConstants.TARGET_V1_REQUEST_MAPPING).contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()).andReturn(); + + assertThat(targetManagement.countTargetsAll()).isEqualTo(0); + + // verify response json exception message + final ExceptionInfo exceptionInfo = ResourceUtility + .convertException(mvcResult.getResponse().getContentAsString()); + assertThat(exceptionInfo.getErrorCode()).isEqualTo(SpServerError.SP_REST_BODY_NOT_READABLE.getKey()); + assertThat(exceptionInfo.getMessage()).isEqualTo(SpServerError.SP_REST_BODY_NOT_READABLE.getMessage()); + } + + @Test + public void createTargetWithBadPayloadBadRequest() throws Exception { + final String notJson = "abc"; + + final MvcResult mvcResult = mvc + .perform(post(RestConstants.TARGET_V1_REQUEST_MAPPING).content(notJson) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()).andReturn(); + + assertThat(targetManagement.countTargetsAll()).isEqualTo(0); + + // verify response json exception message + final ExceptionInfo exceptionInfo = ResourceUtility + .convertException(mvcResult.getResponse().getContentAsString()); + assertThat(exceptionInfo.getExceptionClass()).isEqualTo(MessageNotReadableException.class.getName()); + assertThat(exceptionInfo.getErrorCode()).isEqualTo(SpServerError.SP_REST_BODY_NOT_READABLE.getKey()); + } + + @Test + public void createTargetsListReturnsSuccessful() throws Exception { + final Target test1 = new Target("id1"); + test1.setDescription("testid1"); + test1.setName("testname1"); + final Target test2 = new Target("id2"); + test2.setDescription("testid2"); + test2.setName("testname2"); + final Target test3 = new Target("id3"); + test3.setName("testname3"); + test3.setDescription("testid3"); + + final List targets = new ArrayList<>(); + targets.add(test1); + targets.add(test2); + targets.add(test3); + + final MvcResult mvcResult = mvc + .perform(post("/rest/v1/targets/").content(JsonBuilder.targets(targets)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("[0].name", equalTo("testname1"))) + .andExpect(jsonPath("[0].controllerId", equalTo("id1"))) + .andExpect(jsonPath("[0].description", equalTo("testid1"))) + .andExpect(jsonPath("[0].createdAt", not(equalTo(0)))) + .andExpect(jsonPath("[0].createdBy", equalTo("bumlux"))) + .andExpect(jsonPath("[1].name", equalTo("testname2"))) + .andExpect(jsonPath("[1].createdBy", equalTo("bumlux"))) + .andExpect(jsonPath("[1].controllerId", equalTo("id2"))) + .andExpect(jsonPath("[1].description", equalTo("testid2"))) + .andExpect(jsonPath("[1].createdAt", not(equalTo(0)))) + .andExpect(jsonPath("[1].createdBy", equalTo("bumlux"))) + .andExpect(jsonPath("[2].name", equalTo("testname3"))) + .andExpect(jsonPath("[2].controllerId", equalTo("id3"))) + .andExpect(jsonPath("[2].description", equalTo("testid3"))) + .andExpect(jsonPath("[2].createdAt", not(equalTo(0)))) + .andExpect(jsonPath("[2].createdBy", equalTo("bumlux"))).andReturn(); + + assertThat( + JsonPath.compile("[0]_links.self.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/targets/id1"); + assertThat( + JsonPath.compile("[1]_links.self.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/targets/id2"); + assertThat( + JsonPath.compile("[2]_links.self.href").read(mvcResult.getResponse().getContentAsString()).toString()) + .isEqualTo("http://localhost/rest/v1/targets/id3"); + + assertThat(targetManagement.findTargetByControllerID("id1")).isNotNull(); + assertThat(targetManagement.findTargetByControllerID("id1").getName()).isEqualTo("testname1"); + assertThat(targetManagement.findTargetByControllerID("id1").getDescription()).isEqualTo("testid1"); + assertThat(targetManagement.findTargetByControllerID("id2")).isNotNull(); + assertThat(targetManagement.findTargetByControllerID("id2").getName()).isEqualTo("testname2"); + assertThat(targetManagement.findTargetByControllerID("id2").getDescription()).isEqualTo("testid2"); + assertThat(targetManagement.findTargetByControllerID("id3")).isNotNull(); + assertThat(targetManagement.findTargetByControllerID("id3").getName()).isEqualTo("testname3"); + assertThat(targetManagement.findTargetByControllerID("id3").getDescription()).isEqualTo("testid3"); + } + + @Test + public void createTargetsSingleEntryListReturnsSuccessful() throws Exception { + final String knownName = "someName"; + final String knownControllerId = "controllerId1"; + final String knownDescription = "someDescription"; + final String createTargetsJson = getCreateTargetsListJsonString(knownControllerId, knownName, knownDescription); + + mvc.perform(post(RestConstants.TARGET_V1_REQUEST_MAPPING).content(createTargetsJson) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().is2xxSuccessful()); + + final Slice findTargetsAll = targetManagement.findTargetsAll(new PageRequest(0, 100)); + final Target target = findTargetsAll.getContent().get(0); + assertThat(targetManagement.countTargetsAll()).isEqualTo(1); + assertThat(target.getControllerId()).isEqualTo(knownControllerId); + assertThat(target.getName()).isEqualTo(knownName); + assertThat(target.getDescription()).isEqualTo(knownDescription); + } + + @Test + public void createTargetsSingleEntryListDoubleReturnConflict() throws Exception { + final String knownName = "someName"; + final String knownControllerId = "controllerId1"; + final String knownDescription = "someDescription"; + final String createTargetsJson = getCreateTargetsListJsonString(knownControllerId, knownName, knownDescription); + + // create a taret first to provoke a already exists error + + mvc.perform(post(RestConstants.TARGET_V1_REQUEST_MAPPING).content(createTargetsJson) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().is2xxSuccessful()); + // create another one to retrieve the entity already exists exception + final MvcResult mvcResult = mvc + .perform(post(RestConstants.TARGET_V1_REQUEST_MAPPING).content(createTargetsJson) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is(HttpStatus.CONFLICT.value())).andReturn(); + + // verify only one entry + assertThat(targetManagement.countTargetsAll()).isEqualTo(1); + + // verify response json exception message + final ExceptionInfo exceptionInfo = ResourceUtility + .convertException(mvcResult.getResponse().getContentAsString()); + assertThat(exceptionInfo.getExceptionClass()).isEqualTo(EntityAlreadyExistsException.class.getName()); + assertThat(exceptionInfo.getErrorCode()).isEqualTo(SpServerError.SP_REPO_ENTITY_ALRREADY_EXISTS.getKey()); + assertThat(exceptionInfo.getMessage()).isEqualTo(SpServerError.SP_REPO_ENTITY_ALRREADY_EXISTS.getMessage()); + } + + @Test + public void createTargetsSingleEntryListWithAdditionalNotExistingTargetAttributeWillBeIgnored() throws Exception { + final String knownName = "someName"; + final String knownControllerId = "controllerId1"; + final String knownDescription = "someDescription"; + final String createTargetsJson = getCreateTargetsListJsonStringWithAdditionalNotExistingAttribute( + knownControllerId, knownName, knownDescription); + + mvc.perform(post(RestConstants.TARGET_V1_REQUEST_MAPPING).content(createTargetsJson) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().is2xxSuccessful()); + + final Slice findTargetsAll = targetManagement.findTargetsAll(new PageRequest(0, 100)); + final Target target = findTargetsAll.getContent().get(0); + assertThat(targetManagement.countTargetsAll()).isEqualTo(1); + assertThat(target.getControllerId()).isEqualTo(knownControllerId); + assertThat(target.getName()).isEqualTo(knownName); + assertThat(target.getDescription()).isEqualTo(knownDescription); + } + + @Test + public void getActionWithEmptyResult() throws Exception { + final String knownTargetId = "targetId"; + final Target target = new Target(knownTargetId); + targetManagement.createTarget(target); + + mvc.perform(get( + RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + RestConstants.TARGET_V1_ACTIONS)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("size", equalTo(0))) + .andExpect(jsonPath("content", hasSize(0))).andExpect(jsonPath("total", equalTo(0))); + } + + @Test + public void getCancelAction() throws Exception { + final String knownTargetId = "targetId"; + final List actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId); + + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/" + actions.get(0).getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("id", equalTo(actions.get(0).getId().intValue()))) + .andExpect(jsonPath("type", equalTo("cancel"))).andExpect(jsonPath("status", equalTo("pending"))) + .andExpect(jsonPath("_links.self.href", equalTo(generateActionSelfLink(knownTargetId, actions.get(0))))) + .andExpect(jsonPath("_links.canceledaction.href", + equalTo(generateCanceledactionreferenceLink(knownTargetId, actions.get(0))))) + .andExpect(jsonPath("_links.status.href", + equalTo(generateStatusreferenceLink(knownTargetId, actions.get(0))))); + } + + @Test + public void getMultipleActions() throws Exception { + final String knownTargetId = "targetId"; + final List actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId); + + mvc.perform(get( + RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + RestConstants.TARGET_V1_ACTIONS)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("content.[1].id", equalTo(actions.get(1).getId().intValue()))) + .andExpect(jsonPath("content.[1].type", equalTo("update"))) + .andExpect(jsonPath("content.[1].status", equalTo("pending"))) + .andExpect(jsonPath("content.[1]._links.self.href", + equalTo(generateActionSelfLink(knownTargetId, actions.get(1))))) + .andExpect(jsonPath("content.[0].id", equalTo(actions.get(0).getId().intValue()))) + .andExpect(jsonPath("content.[0].type", equalTo("cancel"))) + .andExpect(jsonPath("content.[0].status", equalTo("pending"))) + .andExpect(jsonPath("content.[0]._links.self.href", + equalTo(generateActionSelfLink(knownTargetId, actions.get(0))))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(2))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(2))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(2))); + } + + @Test + public void getMultipleActionsWithPagingLimitRequestParameter() throws Exception { + final String knownTargetId = "targetId"; + final List actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId); + + // page 1: one entry + mvc.perform(get( + RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + RestConstants.TARGET_V1_ACTIONS) + .param(RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(1))) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("content.[0].id", equalTo(actions.get(0).getId().intValue()))) + .andExpect(jsonPath("content.[0].type", equalTo("cancel"))) + .andExpect(jsonPath("content.[0].status", equalTo("pending"))) + .andExpect(jsonPath("content.[0]._links.self.href", + equalTo(generateActionSelfLink(knownTargetId, actions.get(0))))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(2))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(1))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(1))); + + // page 2: one entry + mvc.perform(get( + RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + RestConstants.TARGET_V1_ACTIONS) + .param(RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(1)) + .param(RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, String.valueOf(1))) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("content.[0].id", equalTo(actions.get(1).getId().intValue()))) + .andExpect(jsonPath("content.[0].type", equalTo("update"))) + .andExpect(jsonPath("content.[0].status", equalTo("pending"))) + .andExpect(jsonPath("content.[0]._links.self.href", + equalTo(generateActionSelfLink(knownTargetId, actions.get(1))))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(2))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(1))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(1))); + } + + private String generateActionSelfLink(final String knownTargetId, final Action action) { + return "http://localhost" + RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/" + action.getId(); + } + + private String generateCanceledactionreferenceLink(final String knownTargetId, final Action action) { + return "http://localhost" + RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/" + action.getId(); + } + + private String generateStatusreferenceLink(final String knownTargetId, final Action action) { + return "http://localhost" + RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/" + action.getId() + "/" + RestConstants.TARGET_V1_ACTION_STATUS + + "?offset=0&limit=50&sort=id:DESC"; + } + + private List generateTargetWithTwoUpdatesWithOneOverride(final String knownTargetId) { + + final PageRequest pageRequest = new PageRequest(0, 100, Direction.ASC, ActionStatusFields.ID.getFieldName()); + + Target target = new Target(knownTargetId); + target = targetManagement.createTarget(target); + final List targets = new ArrayList<>(); + targets.add(target); + + final Iterator sets = TestDataUtil + .generateDistributionSets(2, softwareManagement, distributionSetManagement).iterator(); + final DistributionSet one = sets.next(); + final DistributionSet two = sets.next(); + + // Update + final List updatedTargets = deploymentManagement.assignDistributionSet(one, targets) + .getAssignedTargets(); + // 2nd update + deploymentManagement.assignDistributionSet(two, updatedTargets); + + // two updates, one cancelation + final List actions = actionRepository.findAll(pageRequest).getContent(); + + assertThat(actions).hasSize(2); + return actions; + } + + @Test + public void getUpdateAction() throws Exception { + final String knownTargetId = "targetId"; + final List actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId); + + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/" + actions.get(1).getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("id", equalTo(actions.get(1).getId().intValue()))) + .andExpect(jsonPath("type", equalTo("update"))).andExpect(jsonPath("status", equalTo("pending"))) + .andExpect(jsonPath("_links.self.href", equalTo(generateActionSelfLink(knownTargetId, actions.get(1))))) + .andExpect(jsonPath("_links.distributionset.href", + equalTo("http://localhost/rest/v1/distributionsets/" + + actions.get(1).getDistributionSet().getId()))) + .andExpect(jsonPath("_links.status.href", + equalTo(generateStatusreferenceLink(knownTargetId, actions.get(1))))); + } + + @Test + public void getActionStatusWithMultipleResultsWithPagingLimitRequestParameter() throws Exception { + final int limitSize = 1; + final String knownTargetId = "targetId"; + final List actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId); + actions.get(0).setStatus(Status.RUNNING); + controllerManagament.addUpdateActionStatus( + new ActionStatus(actions.get(0), Status.RUNNING, System.currentTimeMillis(), "testmessage"), + actions.get(0)); + + final PageRequest pageRequest = new PageRequest(0, 1000, Direction.ASC, ActionStatusFields.ID.getFieldName()); + + // limit to 1 - first page -> standard cancel message + Long reportAt = deploymentManagement + .findActionsByTarget(pageRequest, targetManagement.findTargetByControllerID(knownTargetId)).getContent() + .get(0).getCreatedAt(); + Long id = deploymentManagement + .findActionsByTarget(pageRequest, targetManagement.findTargetByControllerID(knownTargetId)).getContent() + .get(0).getId(); + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/" + actions.get(0).getId() + "/status") + .param(RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(limitSize))) + .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(3))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(limitSize))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(limitSize))) + .andExpect(jsonPath("content.[0].id", equalTo(id.intValue()))) + .andExpect(jsonPath("content.[0].type", equalTo("running"))) + .andExpect(jsonPath("content.[0].messages", hasSize(1))) + .andExpect(jsonPath("content.[0].reportedAt", equalTo(reportAt))); + + // limit to 1 - first page -> added custom message + reportAt = deploymentManagement + .findActionsByTarget(pageRequest, targetManagement.findTargetByControllerID(knownTargetId)).getContent() + .get(1).getCreatedAt(); + id = deploymentManagement + .findActionsByTarget(pageRequest, targetManagement.findTargetByControllerID(knownTargetId)).getContent() + .get(1).getCreatedAt(); + + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/" + actions.get(0).getId() + "/status") + .param(RestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(limitSize)) + .param(RestConstants.REQUEST_PARAMETER_PAGING_OFFSET, String.valueOf(1))) + .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(3))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(1))); + } + + @Test + public void assignDistributionSetToTarget() throws Exception { + + final Target target = targetManagement.createTarget(new Target("fsdfsd")); + final DistributionSet set = TestDataUtil.generateDistributionSet("one", softwareManagement, + distributionSetManagement); + + mvc.perform(post(RestConstants.TARGET_V1_REQUEST_MAPPING + "/fsdfsd/assignedDS") + .content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + assertThat(targetManagement.findTargetByControllerID("fsdfsd").getAssignedDistributionSet()).isEqualTo(set); + } + + @Test + public void assignDistributionSetToTargetWithActionTimeForcedAndTime() throws Exception { + + final Target target = targetManagement.createTarget(new Target("fsdfsd")); + final DistributionSet set = TestDataUtil.generateDistributionSet("one", softwareManagement, + distributionSetManagement); + + final long forceTime = System.currentTimeMillis(); + final String body = new JSONObject().put("id", set.getId()).put("type", "timeforced") + .put("forcetime", forceTime).toString(); + + mvc.perform(post(RestConstants.TARGET_V1_REQUEST_MAPPING + "/fsdfsd/assignedDS").content(body) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + final List findActiveActionsByTarget = deploymentManagement.findActiveActionsByTarget(target); + assertThat(findActiveActionsByTarget).hasSize(1); + assertThat(findActiveActionsByTarget.get(0).getActionType()).isEqualTo(ActionType.TIMEFORCED); + assertThat(findActiveActionsByTarget.get(0).getForcedTime()).isEqualTo(forceTime); + assertThat(targetManagement.findTargetByControllerID("fsdfsd").getAssignedDistributionSet()).isEqualTo(set); + } + + @Test + public void invalidRequestsOnAssignDistributionSetToTarget() throws Exception { + + final DistributionSet set = TestDataUtil.generateDistributionSet("one", softwareManagement, + distributionSetManagement); + + mvc.perform(post(RestConstants.TARGET_V1_REQUEST_MAPPING + "/fsdfsd/assignedDS") + .content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + targetManagement.createTarget(new Target("fsdfsd")); + + mvc.perform(post(RestConstants.TARGET_V1_REQUEST_MAPPING + "/fsdfsd/assignedDS") + .content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + mvc.perform(post(RestConstants.TARGET_V1_REQUEST_MAPPING + "/fsdfsd/assignedDS").content("{\"id\":12345678}") + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + mvc.perform(delete(RestConstants.TARGET_V1_REQUEST_MAPPING + "/fsdfsd/assignedDS") + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + mvc.perform(put(RestConstants.TARGET_V1_REQUEST_MAPPING + "/fsdfsd/assignedDS") + .content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + + } + + @Test + public void invalidRequestsOnActionResource() throws Exception { + final String knownTargetId = "targetId"; + + // target does not exist + mvc.perform(get( + RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + RestConstants.TARGET_V1_ACTIONS)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + final List actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId); + final Long actionId = actions.get(0).getId(); + + // should work now + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/" + actionId)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + // action does not exist + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/12321")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + // not allowed methods + mvc.perform(put(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/" + actionId)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + mvc.perform(post(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/" + actionId)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + } + + @Test + public void invalidRequestsOnActionStatusResource() throws Exception { + final String knownTargetId = "targetId"; + + // target does not exist + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/1/status")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + final List actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId); + final Long actionId = actions.get(0).getId(); + + // should work now + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/" + actionId + "/status")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + // action does not exist + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/12321/status")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + // not allowed methods + mvc.perform(delete(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/" + actionId + "/status")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + mvc.perform(put(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/" + actionId + "/status")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + mvc.perform(post(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + RestConstants.TARGET_V1_ACTIONS + "/" + actionId + "/status")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + } + + @Test + public void getControllerAttributesViaTargetResourceReturnsAttributesWithOk() throws Exception { + // create target with attributes + final String knownTargetId = "targetIdWithAttributes"; + final Map knownControllerAttrs = new HashMap<>(); + knownControllerAttrs.put("a", "1"); + knownControllerAttrs.put("b", "2"); + final Target target = new Target(knownTargetId); + targetManagement.createTarget(target); + controllerManagament.updateControllerAttributes(knownTargetId, knownControllerAttrs); + + // test query target over rest resource + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/attributes")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.a", equalTo("1"))).andExpect(jsonPath("$.b", equalTo("2"))); + } + + @Test + public void getControllerEmptyAttributesReturnsNoContent() throws Exception { + // create target with attributes + final String knownTargetId = "targetIdWithAttributes"; + final Target target = new Target(knownTargetId); + targetManagement.createTarget(target); + + // test query target over rest resource + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/attributes")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().is(HttpStatus.NO_CONTENT.value())); + } + + @Test + public void searchTargetsUsingRsqlQuery() throws Exception { + final int amountTargets = 10; + createTargetsAlphabetical(amountTargets); + + final String rsqlFindAOrB = "controllerId==a,controllerId==b"; + + mvc.perform(get(RestConstants.TARGET_V1_REQUEST_MAPPING + "?q=" + rsqlFindAOrB)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andExpect(jsonPath("size", equalTo(2))) + .andExpect(jsonPath("total", equalTo(2))).andExpect(jsonPath("content[0].controllerId", equalTo("a"))) + .andExpect(jsonPath("content[1].controllerId", equalTo("b"))); + } + + private String getCreateTargetsListJsonString(final String controllerId, final String name, + final String description) { + return "[{\"name\":\"" + name + "\",\"controllerId\":\"" + controllerId + "\",\"description\":\"" + description + + "\"}]"; + } + + private String getCreateTargetsListJsonStringWithAdditionalNotExistingAttribute(final String controllerId, + final String name, final String description) { + return "[{\"name\":\"" + name + "\",\"controllerId\":\"" + controllerId + "\",\"description\":\"" + description + + "\"}]"; + } + + private void createSingleTarget(final String controllerId, final String name) { + final Target target = new Target(controllerId); + target.setName(name); + target.setDescription(TARGET_DESCRIPTION_TEST); + targetManagement.createTarget(target); + controllerManagament.updateLastTargetQuery(controllerId, null); + } + + /** + * creating targets with the given amount by setting name, id etc from the + * alphabet [a-z] using the ASCII. + * + * @param amount + */ + private void createTargetsAlphabetical(final int amount) { + char character = 'a'; + for (int index = 0; index < amount; index++) { + final String str = String.valueOf(character); + final Target target = new Target(str); + target.setName(str); + target.setDescription(str); + final Target savedTarget = targetManagement.createTarget(target); + assertThat(savedTarget.getLastModifiedBy()).isNotNull(); + character++; + } + } + + /** + * helper method to give feedback mark an target IN_SNCY + * + * @param controllerId + * the controller id to give feedback to + * @param savedSet + * the distribution set + * @throws Exception + * @throws JSONException + */ + private void feedbackToByInSync(final String controllerId, final DistributionSet savedSet) + throws Exception, JSONException { + final Pageable pageReq = new PageRequest(0, 100); + final Action action = actionRepository.findByDistributionSet(pageReq, savedSet).getContent().get(0); + + final ActionStatus actionStatus = new ActionStatus(action, Status.FINISHED, 0l); + controllerManagement.addUpdateActionStatus(actionStatus, action); + } +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/model/ExceptionInfoTest.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/model/ExceptionInfoTest.java new file mode 100755 index 000000000..e9601d693 --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/model/ExceptionInfoTest.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model; + +import static org.fest.assertions.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +public class ExceptionInfoTest { + + @Test + public void setterAndGetter() { + final String knownExceptionClass = "hawkbit.test.exception.Class"; + final String knownErrorCode = "hawkbit.error.code.Known"; + final String knownMessage = "a known message"; + final List knownParameters = new ArrayList<>(); + knownParameters.add("param1"); + knownParameters.add("param2"); + + final ExceptionInfo underTest = new ExceptionInfo(); + underTest.setErrorCode(knownErrorCode); + underTest.setExceptionClass(knownExceptionClass); + underTest.setMessage(knownMessage); + underTest.setParameters(knownParameters); + + assertThat(underTest.getErrorCode()).isEqualTo(knownErrorCode); + assertThat(underTest.getExceptionClass()).isEqualTo(knownExceptionClass); + assertThat(underTest.getMessage()).isEqualTo(knownMessage); + assertThat(underTest.getParameters()).isEqualTo(knownParameters); + } + +} diff --git a/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/model/PagedListTest.java b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/model/PagedListTest.java new file mode 100755 index 000000000..0c47856dd --- /dev/null +++ b/hawkbit-rest-resource/src/test/java/org/eclipse/hawkbit/rest/resource/model/PagedListTest.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.resource.model; + +import static org.fest.assertions.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +public class PagedListTest { + + @Test(expected = NullPointerException.class) + public void createListWithNullContentThrowsException() { + new PagedList<>(null, 0); + } + + @Test + public void createListWithContent() { + final long knownTotal = 2; + final List knownContentList = new ArrayList<>(); + knownContentList.add("content1"); + knownContentList.add("content2"); + + final PagedList pagedList = new PagedList<>(knownContentList, knownTotal); + + assertThat(pagedList.getTotal()).isEqualTo(knownTotal); + assertThat(pagedList.getSize()).isEqualTo(knownContentList.size()); + } + + @Test + public void createListWithSmallerTotalThanContentSizeIsOk() { + final long knownTotal = 0; + final List knownContentList = new ArrayList<>(); + knownContentList.add("content1"); + knownContentList.add("content2"); + + final PagedList pagedList = new PagedList<>(knownContentList, knownTotal); + assertThat(pagedList.getTotal()).isEqualTo(knownTotal); + assertThat(pagedList.getSize()).isEqualTo(knownContentList.size()); + + } +} diff --git a/hawkbit-rest-resource/src/test/resources/application-test.properties b/hawkbit-rest-resource/src/test/resources/application-test.properties new file mode 100755 index 000000000..bdd959ca2 --- /dev/null +++ b/hawkbit-rest-resource/src/test/resources/application-test.properties @@ -0,0 +1,63 @@ +# +# Copyright (c) 2015 Bosch Software Innovations GmbH and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# + +# used if IM profile is disabled +security.ignored=true + +# IM required for integration tests +spring.profiles.active=im,suiteembedded,artifactrepository,redis + +server.port=12222 + +spring.data.mongodb.uri=mongodb://localhost/spArtifactRepository${random.value} +spring.data.mongodb.port=28017 + + +# supported: H2, MYSQL +hawkbit.server.database=H2 +hawkbit.server.database.env=TEST +spring.main.show_banner=false + +hawkbit.server.controller.security.authentication.header=true + +hawkbit.server.artifact.repo.upload.maxFileSize=5MB + +hawkbit.server.security.dos.maxStatusEntriesPerAction=100 + +hawkbit.server.security.dos.maxAttributeEntriesPerTarget=10 + +spring.jpa.database=${hawkbit.server.database} +#spring.jpa.show-sql=true + + +flyway.sqlMigrationSuffix=${spring.jpa.database}.sql + +# effective DB setting +spring.datasource.url=${${hawkbit.server.database}.spring.datasource.url} +spring.datasource.driverClassName=${${hawkbit.server.database}.spring.datasource.driverClassName} +spring.datasource.username=${${hawkbit.server.database}.spring.datasource.username} +spring.datasource.password=${${hawkbit.server.database}.spring.datasource.password} + +# H2 +##;AUTOCOMMIT=ON +H2.spring.datasource.url=jdbc:h2:mem:sp-db;DB_CLOSE_ON_EXIT=FALSE +#H2.spring.datasource.url=jdbc:h2:./db/sp-db;AUTO_SERVER=TRUE;DB_CLOSE_ON_EXIT=FALSE;MVCC=TRUE +H2.spring.datasource.driverClassName=org.h2.Driver +H2.spring.datasource.username=sa +H2.spring.datasource.password=sa + +# MYSQL +MYSQL.spring.datasource.url=jdbc:mysql://localhost:3306/sp_test +MYSQL.spring.datasource.driverClassName=org.mariadb.jdbc.Driver +MYSQL.spring.datasource.username=root +MYSQL.spring.datasource.password= + +# SP Controller configuration +hawkbit.controller.pollingTime=00:01:00 +hawkbit.controller.pollingOverdueTime=00:01:00 diff --git a/hawkbit-security-core/pom.xml b/hawkbit-security-core/pom.xml new file mode 100755 index 000000000..2c041260b --- /dev/null +++ b/hawkbit-security-core/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + + org.eclipse.hawkbit + hawkbit-parent + 0.1.0-SNAPSHOT + + hawkbit-security-core + hawkBit :: Core Security + + + + + org.eclipse.hawkbit + hawkbit-core + ${project.version} + + + javax.servlet + javax.servlet-api + + + org.springframework + spring-web + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + com.google.guava + guava + 18.0 + + + org.slf4j + slf4j-api + + + org.springframework.data + spring-data-commons + + + org.springframework.security + spring-security-core + + + org.springframework.boot + spring-boot + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.easytesting + fest-assert-core + test + + + org.easytesting + fest-assert + test + + + ru.yandex.qatools.allure + allure-junit-adaptor + test + + + + \ No newline at end of file diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/ExcludePathAwareShallowETagFilter.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/ExcludePathAwareShallowETagFilter.java new file mode 100755 index 000000000..195cd4fea --- /dev/null +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/ExcludePathAwareShallowETagFilter.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.util.AntPathMatcher; +import org.springframework.web.filter.ShallowEtagHeaderFilter; + +/** + * An {@link ShallowEtagHeaderFilter} with exclusion paths to exclude some paths + * where no ETag header should be generated due that calculating the ETag is an + * expensive operation and the response output need to be copied in memory which + * should be excluded in case of artifact downloads which could be big of size. + * + * + * + * + */ +public class ExcludePathAwareShallowETagFilter extends ShallowEtagHeaderFilter { + + private final String[] excludeAntPaths; + private final AntPathMatcher antMatcher; + + /** + * @param excludeAntPaths + */ + public ExcludePathAwareShallowETagFilter(final String... excludeAntPaths) { + this.excludeAntPaths = excludeAntPaths; + this.antMatcher = new AntPathMatcher(); + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.web.filter.ShallowEtagHeaderFilter#doFilterInternal( + * javax.servlet.http .HttpServletRequest, + * javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain) + */ + @Override + protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, + final FilterChain filterChain) throws ServletException, IOException { + final boolean shouldExclude = shouldExclude(request); + if (shouldExclude) { + filterChain.doFilter(request, response); + } else { + super.doFilterInternal(request, response, filterChain); + } + } + + private boolean shouldExclude(final HttpServletRequest request) { + for (final String pattern : excludeAntPaths) { + if (antMatcher.match(request.getContextPath() + pattern, request.getRequestURI())) { + // exclude this request from eTag filter + return true; + } + } + return false; + } +} diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/MultitenancyIndicator.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/MultitenancyIndicator.java new file mode 100755 index 000000000..fe5b0e6a5 --- /dev/null +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/MultitenancyIndicator.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.im.authentication; + +import org.springframework.security.authentication.AuthenticationProvider; + +/** + * Indicates if the SP server runs in multi-tenancy mode. By means e.g. if a + * login screen needs to allow to specifiy the tenant to login. + * + * This can defere e.g. in case if the {@link AuthenticationProvider} allows + * {@link TenantUserPasswordAuthenticationToken} tokens or not. + * + * + * + */ +public interface MultitenancyIndicator { + + /** + * @return {@code true} if multi-tenancy is supported, otherwise + * {@code false}. + */ + boolean isMultiTenancySupported(); + +} diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/PermissionService.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/PermissionService.java new file mode 100755 index 000000000..63ec4479a --- /dev/null +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/PermissionService.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.im.authentication; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * + * + */ +public class PermissionService { + + /** + * Checks if the given {@code permission} contains in the. In case no + * {@code context} is available {@code false} will be returned. + * + * @param permission + * the permission to check against the + * @return {@code true} if a is available and contains the given + * {@code permission}, otherwise {@code false}. + * @see SpPermission + */ + public boolean hasPermission(final String permission) { + final SecurityContext context = SecurityContextHolder.getContext(); + if (context == null) { + return false; + } + final Authentication authentication = context.getAuthentication(); + if (authentication == null) { + return false; + } + + for (final GrantedAuthority authority : authentication.getAuthorities()) { + if (authority.getAuthority().equals(permission)) { + return true; + } + } + + return false; + } + + public List getAllPermission() { + final List permissions = new ArrayList(); + final SecurityContext context = SecurityContextHolder.getContext(); + if (context == null) { + return permissions; + } + final Authentication authentication = context.getAuthentication(); + if (authentication == null) { + return permissions; + } + + authentication.getAuthorities().stream().forEach(authority -> permissions.add(authority.getAuthority())); + + return permissions; + } + + /** + * Checks if at least on permission of the given {@code permissions} + * contains in the . In case no {@code context} is available {@code false} + * will be returned. + * + * @param permissions + * the permissions to check against the + * @return {@code true} if a is available and contains the given + * {@code permission}, otherwise {@code false}. + * @see SpPermission + */ + public boolean hasAtLeastOnePermission(final List permissions) { + final SecurityContext context = SecurityContextHolder.getContext(); + if (context != null) { + final Authentication authentication = context.getAuthentication(); + if (authentication != null) { + for (final GrantedAuthority authority : authentication.getAuthorities()) { + for (final String permission : permissions) { + if (authority.getAuthority().equals(permission)) { + return true; + } + } + } + } + } + return false; + } + +} diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java new file mode 100755 index 000000000..2e46d74c7 --- /dev/null +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java @@ -0,0 +1,283 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.im.authentication; + +import java.lang.annotation.Target; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.GrantedAuthority; + +/** + *

+ * Software provisioning permissions that are technically available as + * {@link GrantedAuthority} based on the authenticated users identity context. + *

+ * + *

+ * The Permissions cover CRUD for two data areas of SP:
+ *
+ * XX_Target_CRUD which covers the following entities: {@link Target} entities + * including metadata, {@link TargetTag}s, {@link Action}s, + * {@link TargetRegistrationRule}s
+ * XX_Repository CRUD which covers: {@link DistributionSet}s, + * {@link SoftwareModule}s, DS Tags
+ *

+ * + * + * + * + * + */ +public final class SpPermission { + + /** + * Permission to read the targets from the + * {@link ProvisioningTargetRepository} including their meta information, + * {@link ProvisioningTargetFilter}s and target changing entities ( + * {@link DistributionSetApplier} and {@link TargetRegistrationRule}). That + * corresponds in REST API to GET. + */ + public static final String READ_TARGET = "READ_TARGET"; + + /** + * Permission to read the target security token. The security token is + * security concerned and should be protected. So the combination + * {@link #READ_TARGET} and {@link #READ_TARGET_SEC_TOKEN} is necessary to + * able to read the security token of an target. + */ + public static final String READ_TARGET_SEC_TOKEN = "READ_TARGET_SECURITY_TOKEN"; + + /** + * Permission to change/edit/update targets in the + * {@link ProvisioningTargetRepository} including their meta information and + * or/relations or {@link DistributionSet} assignment, + * {@link ProvisioningTargetFilter}s and target changing entities ( + * {@link DistributionSetApplier} and {@link TargetRegistrationRule}). That + * corresponds in REST API to POST. + */ + public static final String UPDATE_TARGET = "UPDATE_TARGET"; + + /** + * Permission to add new targets to the {@link ProvisioningTargetRepository} + * including their meta information and or/relations or + * {@link DistributionSet} assignment.That corresponds in REST API to PUT. + */ + public static final String CREATE_TARGET = "CREATE_TARGET"; + + /** + * Permission to delete targets in the {@link ProvisioningTargetRepository}, + * {@link ProvisioningTargetFilter}s and target changing entities ( + * {@link DistributionSetApplier} and {@link TargetRegistrationRule}). That + * corresponds in REST API to DELETE. + */ + public static final String DELETE_TARGET = "DELETE_TARGET"; + + /** + * Permission to read {@link DistributionSet}s and/or {@link OsPackage}s. + * That corresponds in REST API to GET. + */ + public static final String READ_REPOSITORY = "READ_REPOSITORY"; + + /** + * Permission to edit/update {@link DistributionSet}s including their + * {@link OsPackage} assignment and/or {@link OsPackage}s. That corresponds + * in REST API to POST. + */ + public static final String UPDATE_REPOSITORY = "UPDATE_REPOSITORY"; + + /** + * Permission to add {@link DistributionSet}s and/or {@link OsPackage}s to + * the repository. That corresponds in REST API to PUT. + */ + public static final String CREATE_REPOSITORY = "CREATE_REPOSITORY"; + + /** + * Permission to delete {@link DistributionSet}s and/or {@link OsPackage}s + * from the repository. That corresponds in REST API to DELETE. + */ + public static final String DELETE_REPOSITORY = "DELETE_REPOSITORY"; + + /** + * Permission to monitor the SP system. E.g. retrieving health, monitor + * checks through REST API provided by the spring actuator. + */ + public static final String SYSTEM_MONITOR = "SYSTEM_MONITOR"; + + /** + * Permission to retrieve diagnosis of the SP system. E.g. retrieving + * metrics, configuration through REST API provided by the spring actuator. + */ + public static final String SYSTEM_DIAG = "SYSTEM_DIAG"; + + /** + * Permission to administrate the system on a global, i.e. tenant + * independent scale. Thta inlcuds the deletion of tenants. + */ + public static final String SYSTEM_ADMIN = "SYSTEM_ADMIN"; + + /** + * Permission to download repository artifact of an software module. + */ + public static final String DOWNLOAD_REPOSITORY_ARTIFACT = "DOWNLOAD_REPOSITORY_ARTIFACT"; + + /** + * Permission to administrate the tenant settings. + */ + public static final String TENANT_CONFIGURATION = "TENANT_CONFIGURATION"; + + private SpPermission() { + // Constants only + } + + /** + * Contains all the spring security evaluation expressions for the + * {@link PreAuthorize} annotation for method security. + *

+ * Examples: + *

+ * {@code + * hasRole([role]) Returns true if the current principal has the specified role. + * hasAnyRole([role1,role2]) Returns true if the current principal has any of the supplied roles (given as a comma-separated list of strings) + * principal Allows direct access to the principal object representing the current user + * authentication Allows direct access to the current Authentication object obtained from the SecurityContext + * permitAll Always evaluates to true + * denyAll Always evaluates to false + * isAnonymous() Returns true if the current principal is an anonymous user + * isRememberMe() Returns true if the current principal is a remember-me user + * isAuthenticated() Returns true if the user is not anonymous + * isFullyAuthenticated() Returns true if the user is not an anonymous or a remember-me user + * } + * + * + * + * + */ + public static final class SpringEvalExpressions { + /* + * Spring security eval expressions. + */ + private static final String HAS_AUTH_PREFIX = "hasAuthority('"; + private static final String HAS_AUTH_SUFFIX = "')"; + private static final String HAS_AUTH_AND = " and "; + + /** + * The role which contains in the spring security context in case an + * controller is authenticated. + */ + public static final String CONTROLLER_ROLE = "ROLE_CONTROLLER"; + + /** + * The role which contains in the spring security context in case an + * controller is authenticated but only as anonymous. + */ + public static final String CONTROLLER_ROLE_ANONYMOUS = "ROLE_CONTROLLER_ANONYMOUS"; + + /** + * The spring security eval expression operator {@code or}. + */ + public static final String HAS_AUTH_OR = " or "; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#UPDATE_TARGET}. + */ + public static final String HAS_AUTH_UPDATE_TARGET = HAS_AUTH_PREFIX + UPDATE_TARGET + HAS_AUTH_SUFFIX; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#SYSTEM_ADMIN}. + */ + public static final String HAS_AUTH_SYSTEM_ADMIN = HAS_AUTH_PREFIX + SYSTEM_ADMIN + HAS_AUTH_SUFFIX; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#READ_TARGET}. + */ + public static final String HAS_AUTH_READ_TARGET = HAS_AUTH_PREFIX + READ_TARGET + HAS_AUTH_SUFFIX; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#CREATE_TARGET}. + */ + public static final String HAS_AUTH_CREATE_TARGET = HAS_AUTH_PREFIX + CREATE_TARGET + HAS_AUTH_SUFFIX; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#DELETE_TARGET}. + */ + public static final String HAS_AUTH_DELETE_TARGET = HAS_AUTH_PREFIX + DELETE_TARGET + HAS_AUTH_SUFFIX; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#READ_REPOSITORY} and + * {@link SpPermission#UPDATE_TARGET}. + */ + public static final String HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET = HAS_AUTH_PREFIX + READ_REPOSITORY + + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + UPDATE_TARGET + HAS_AUTH_SUFFIX; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#CREATE_REPOSITORY}. + */ + public static final String HAS_AUTH_CREATE_REPOSITORY = HAS_AUTH_PREFIX + CREATE_REPOSITORY + HAS_AUTH_SUFFIX; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#DELETE_REPOSITORY}. + */ + public static final String HAS_AUTH_DELETE_REPOSITORY = HAS_AUTH_PREFIX + DELETE_REPOSITORY + HAS_AUTH_SUFFIX; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#READ_REPOSITORY}. + */ + public static final String HAS_AUTH_READ_REPOSITORY = HAS_AUTH_PREFIX + READ_REPOSITORY + HAS_AUTH_SUFFIX; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#UPDATE_REPOSITORY}. + */ + public static final String HAS_AUTH_UPDATE_REPOSITORY = HAS_AUTH_PREFIX + UPDATE_REPOSITORY + HAS_AUTH_SUFFIX; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#READ_REPOSITORY} and + * {@link SpPermission#READ_TARGET}. + */ + public static final String HAS_AUTH_READ_REPOSITORY_AND_READ_TARGET = HAS_AUTH_PREFIX + READ_REPOSITORY + + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + READ_TARGET + HAS_AUTH_SUFFIX; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#DOWNLOAD_REPOSITORY_ARTIFACT}. + */ + public static final String HAS_AUTH_DOWNLOAD_ARTIFACT = HAS_AUTH_PREFIX + DOWNLOAD_REPOSITORY_ARTIFACT + + HAS_AUTH_SUFFIX; + + /** + * Spring security eval hasAnyRole expression to check if the spring + * context contains the anoynmous role or the controller specific role + * {@link SpPermission#CONTROLLER_ROLE}. + */ + public static final String IS_CONTROLLER = "hasAnyRole('" + CONTROLLER_ROLE_ANONYMOUS + "', '" + CONTROLLER_ROLE + + "')"; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#CREATE_REPOSITORY} and + * {@link SpPermission#CREATE_TARGET}. + */ + public static final String HAS_AUTH_CREATE_REPOSITORY_AND_CREATE_TARGET = HAS_AUTH_PREFIX + CREATE_REPOSITORY + + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + CREATE_TARGET + HAS_AUTH_SUFFIX; + + private SpringEvalExpressions() { + // utility class + } + } +} diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/TenantAwareAuthenticationDetails.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/TenantAwareAuthenticationDetails.java new file mode 100755 index 000000000..e524ea054 --- /dev/null +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/TenantAwareAuthenticationDetails.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.im.authentication; + +import java.io.Serializable; + +import org.springframework.security.authentication.AbstractAuthenticationToken; + +/** + * An authentication details object + * {@link AbstractAuthenticationToken#getDetails()} which is stored in the + * spring security authentication token details to transport the principal and + * tenant in the security context session. + * + * + * + * + */ +public class TenantAwareAuthenticationDetails implements Serializable { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final String tenant; + private final boolean controller; + + /** + * @param tenant + * the current tenant + * @param controller + * boolean flag to indicate if this authenticated token is a + * controller authentication. {@code true} in case of + * authenticated controller otherwise {@code false} + */ + public TenantAwareAuthenticationDetails(final String tenant, final boolean controller) { + this.tenant = tenant; + this.controller = controller; + } + + /** + * @return the tenant + */ + public String getTenant() { + return tenant; + } + + /** + * @return the controller + */ + public boolean isController() { + return controller; + } +} diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/TenantUserPasswordAuthenticationToken.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/TenantUserPasswordAuthenticationToken.java new file mode 100755 index 000000000..de712ebb9 --- /dev/null +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/TenantUserPasswordAuthenticationToken.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.im.authentication; + +import java.util.List; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +/** + * The authentication token which transports the username, password and the + * tenant information for authentication. + * + * + * + * + */ +public class TenantUserPasswordAuthenticationToken extends UsernamePasswordAuthenticationToken { + + /** + * + */ + private static final long serialVersionUID = 1L; + + final Object tenant; + + /** + * + * Creating a new {@link TenantUserPasswordAuthenticationToken} as + * {@link #isAuthenticated()} will return {@code false}. + * + * @param tenant + * the tenant to authenticate against + * @param principal + * the principal to authenticate + * @param credentials + * the credentials of the principal + */ + public TenantUserPasswordAuthenticationToken(final Object tenant, final Object principal, + final Object credentials) { + super(principal, credentials); + this.tenant = tenant; + } + + /** + * Creating a new {@link TenantUserPasswordAuthenticationToken} as + * {@link #isAuthenticated()} will return {@code true}. + * + * @param tenant + * the tenant to authenticate against + * @param principal + * the principal to authenticate + * @param credentials + * the credentials of the principal + * @param authorities + * the principal's authorities + */ + public TenantUserPasswordAuthenticationToken(final Object tenant, final Object principal, final Object credentials, + final List authorities) { + super(principal, credentials, authorities); + this.tenant = tenant; + } + + /** + * @return the tenant + */ + public Object getTenant() { + return tenant; + } +} diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/UserAuthenticationFilter.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/UserAuthenticationFilter.java new file mode 100755 index 000000000..b9f690337 --- /dev/null +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/UserAuthenticationFilter.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.im.authentication; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +/** + * Filter to integrate into the SP security filter-chain. The filter is called + * in any remote call through HTTP except the SP login screen. E.g. using the SP + * REST-API. To authenticate user e.g. using Basic-Authentication implement the + * {@link #doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)} + * method. + * + * + * + */ +public interface UserAuthenticationFilter { + + /** + * @see Filter#init(FilterConfig) + * + * @param filterConfig + * the filter config + */ + void init(FilterConfig filterConfig) throws ServletException; + + /** + * @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain) + * + * @param request + * the servlet request + * @param response + * the servlet response + * @param chain + * the filterchain + * @throws IOException + * cannot read from request + * @throws ServletException + * servlet exception + */ + + void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, + ServletException; + + /** + * @see Filter#destroy() + */ + void destroy(); + +} diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/UserPrincipal.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/UserPrincipal.java new file mode 100755 index 000000000..7719a18b9 --- /dev/null +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/UserPrincipal.java @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.im.authentication; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; + +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.userdetails.UserDetails; + +/** + * A software provisioning user principal definition stored in the + * {@link SecurityContext} which contains the user specific attributes. + * + * + * + * + */ +public class UserPrincipal implements UserDetails, Serializable { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final String username; + private final String firstname; + private final String lastname; + private final String loginname; + private final String tenant; + + /** + * @param username + * the user name of the user + * @param firstname + * the first name of the user + * @param lastname + * the last name of the user + * @param loginname + * the login name of user + * @param tenant + * the tenant of the user + */ + public UserPrincipal(final String username, final String firstname, final String lastname, final String loginname, + final String tenant) { + this.username = username; + this.firstname = firstname; + this.lastname = lastname; + this.loginname = loginname; + this.tenant = tenant; + } + + /** + * @return the username + */ + @Override + public String getUsername() { + return username; + } + + /** + * @return the firstname + */ + public String getFirstname() { + return firstname; + } + + /** + * @return the lastname + */ + public String getLastname() { + return lastname; + } + + /** + * @return the loginname + */ + public String getLoginname() { + return loginname; + } + + /** + * @return the tenant + */ + public String getTenant() { + return tenant; + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.security.core.userdetails.UserDetails#getAuthorities( + * ) + */ + @Override + public Collection getAuthorities() { + return Collections.emptyList(); + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.security.core.userdetails.UserDetails#getPassword() + */ + @Override + public String getPassword() { + return null; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.security.core.userdetails.UserDetails# + * isAccountNonExpired() + */ + @Override + public boolean isAccountNonExpired() { + return true; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.security.core.userdetails.UserDetails# + * isAccountNonLocked() + */ + @Override + public boolean isAccountNonLocked() { + return false; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.security.core.userdetails.UserDetails# + * isCredentialsNonExpired() + */ + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.security.core.userdetails.UserDetails#isEnabled() + */ + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/DosFilter.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/DosFilter.java new file mode 100755 index 000000000..86b63cce4 --- /dev/null +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/DosFilter.java @@ -0,0 +1,193 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Pattern; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.hawkbit.util.IpUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.web.filter.OncePerRequestFilter; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +/** + * Filter for protection against denial of service attacks. It reduces the + * maximum number of request per seconds which can be separately configured for + * read (GET) and write (PUT/POST/DELETE) requests. requests + * + * + * + * + */ +public class DosFilter extends OncePerRequestFilter { + + private static final Logger LOG = LoggerFactory.getLogger(DosFilter.class); + private static final Logger LOG_DOS = LoggerFactory.getLogger("server-security.dos"); + private static final Logger LOG_BLACKLIST = LoggerFactory.getLogger("server-security.blacklist"); + + private final Pattern ipAdressBlacklist; + + private final Cache readCountCache = CacheBuilder.newBuilder() + .expireAfterAccess(1, TimeUnit.SECONDS).build(); + + private final Cache writeCountCache = CacheBuilder.newBuilder() + .expireAfterAccess(1, TimeUnit.SECONDS).build(); + + private final Integer maxRead; + private final Integer maxWrite; + + private final Pattern whitelist; + + private final String forwardHeader; + + /** + * Filter constructor including configuration. + * + * @param maxRead + * Maximum number of allowed REST read/GET requests per second + * per client + * @param maxWrite + * Maximum number of allowed REST write/(PUT/POST/etc.) requests + * per second per client + * @param ipDosWhiteListPattern + * {@link Pattern} with with white list of peer IP addresses for + * DOS filter + * @param ipBlackListPattern + * {@link Pattern} with black listed IP addresses + * @param forwardHeader + * the header containing the forwarded IP address e.g. + * {@code x-forwarded-for} + */ + public DosFilter(final Integer maxRead, final Integer maxWrite, final String ipDosWhiteListPattern, + final String ipBlackListPattern, final String forwardHeader) { + super(); + this.maxRead = maxRead; + this.maxWrite = maxWrite; + this.forwardHeader = forwardHeader; + + if (ipBlackListPattern != null && !ipBlackListPattern.isEmpty()) { + ipAdressBlacklist = Pattern.compile(ipBlackListPattern); + } else { + ipAdressBlacklist = null; + } + + if (ipDosWhiteListPattern != null && !ipDosWhiteListPattern.isEmpty()) { + whitelist = Pattern.compile(ipDosWhiteListPattern); + } else { + whitelist = null; + } + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.web.filter.OncePerRequestFilter#doFilterInternal( + * javax.servlet.http. HttpServletRequest, + * javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain) + */ + @Override + protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, + final FilterChain filterChain) throws ServletException, IOException { + + boolean processChain = true; + + final String ip = IpUtil.getClientIpFromRequest(request, forwardHeader).getHost(); + if (checkIpFails(ip)) { + processChain = handleMissingIpAddress(response); + } else { + processChain = checkAgainstBlacklist(response, ip); + + if (processChain && (whitelist == null || !whitelist.matcher(ip).find())) { + // read request + if (HttpMethod.valueOf(request.getMethod()) == HttpMethod.GET) { + processChain = handleReadRequest(response, ip); + } + // write request + else { + processChain = handleWriteRequest(response, ip); + } + } + } + + if (processChain) { + filterChain.doFilter(request, response); + } + + } + + /** + * @return false if the given ip address is on the blacklist and further + * processing of the request if forbidden + */ + private boolean checkAgainstBlacklist(final HttpServletResponse response, final String ip) { + if (ipAdressBlacklist != null && ipAdressBlacklist.matcher(ip).find()) { + LOG_BLACKLIST.info("Blacklisted client ({}) tries to access the server!", ip); + response.setStatus(HttpStatus.FORBIDDEN.value()); + return false; + } + return true; + } + + private boolean checkIpFails(final String ip) { + return ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip); + } + + private boolean handleMissingIpAddress(final HttpServletResponse response) { + boolean processChain; + LOG.error("Failed to get peer IP adress"); + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + processChain = false; + return processChain; + } + + private boolean handleWriteRequest(final HttpServletResponse response, final String ip) { + boolean processChain = true; + final AtomicInteger count = writeCountCache.getIfPresent(ip); + + if (count == null) { + writeCountCache.put(ip, new AtomicInteger()); + } else if (count.getAndIncrement() > maxWrite) { + LOG_DOS.info("Registered DOS attack! Client {} is above configured WRITE request threshold ({})!", ip, + maxWrite); + response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); + processChain = false; + } + + return processChain; + } + + private boolean handleReadRequest(final HttpServletResponse response, final String ip) { + boolean processChain = true; + final AtomicInteger count = readCountCache.getIfPresent(ip); + + if (count == null) { + readCountCache.put(ip, new AtomicInteger()); + } else if (count.getAndIncrement() > maxRead) { + LOG_DOS.info("Registered DOS attack! Client {} is above configured READ request threshold ({})!", ip, + maxRead); + response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); + processChain = false; + } + + return processChain; + } +} diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityContextTenantAware.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityContextTenantAware.java new file mode 100755 index 000000000..a3cb56d91 --- /dev/null +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityContextTenantAware.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * A {@link TenantAware} implemenation which retrieves the ID of the tenant from + * the {@link SecurityContext#getAuthentication()} + * {@link Authentication#getDetails()} which holds the + * {@link TenantAwareAuthenticationDetails} object. + * + * + * + * + */ +public class SecurityContextTenantAware implements TenantAware { + + private static final ThreadLocal TENANT_THREAD_LOCAL = new ThreadLocal<>(); + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.tenancy.TenantAware#getCurrentTenantId() + */ + @Override + public String getCurrentTenant() { + if (TENANT_THREAD_LOCAL.get() != null) { + return TENANT_THREAD_LOCAL.get(); + } + final SecurityContext context = SecurityContextHolder.getContext(); + if (context.getAuthentication() != null) { + final Object authDetails = context.getAuthentication().getDetails(); + if (authDetails instanceof TenantAwareAuthenticationDetails) { + return ((TenantAwareAuthenticationDetails) authDetails).getTenant(); + } + } + return null; + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.tenancy.TenantAware#runAsTenant(java.lang.String, + * java.util.concurrent.Callable) + */ + @Override + public T runAsTenant(final String tenant, final TenantRunner callable) { + TENANT_THREAD_LOCAL.set(tenant); + try { + return callable.run(); + } finally { + TENANT_THREAD_LOCAL.remove(); + } + } +} diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityProperties.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityProperties.java new file mode 100755 index 000000000..b9bf45fd0 --- /dev/null +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityProperties.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * The common properties for security. + * + * + * + */ +@ConfigurationProperties +public class SecurityProperties { + + @Value("${hawkbit.server.controller.security.rp.cnHeader:X-Ssl-Client-Cn}") + private String rpCnHeader; + + @Value("${hawkbit.server.controller.security.rp.sslIssuerHashHeader:X-Ssl-Issuer-Hash-%d}") + private String rpSslIssuerHashHeader; + + @Value("${hawkbit.server.controller.security.rp.trustedIPs:#{null}}") + private List rpTrustedIPs; + + @Value("${hawkbit.server.controller.security.authentication.anonymous.enabled:false}") + private Boolean anonymousEnabled; + + public String getRpCnHeader() { + return rpCnHeader; + } + + public String getRpSslIssuerHashHeader() { + return rpSslIssuerHashHeader; + } + + public List getRpTrustedIPs() { + return rpTrustedIPs; + } + + public Boolean getAnonymousEnabled() { + return anonymousEnabled; + } + + public void setRpCnHeader(final String rpCnHeader) { + this.rpCnHeader = rpCnHeader; + } + + public void setRpSslIssuerHashHeader(final String rpSslIssuerHashHeader) { + this.rpSslIssuerHashHeader = rpSslIssuerHashHeader; + } + + public void setRpTrustedIPs(final List rpTrustedIPs) { + this.rpTrustedIPs = rpTrustedIPs; + } + + public void setAnonymousEnabled(final Boolean anonymousEnabled) { + this.anonymousEnabled = anonymousEnabled; + } +} diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityTokenGenerator.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityTokenGenerator.java new file mode 100755 index 000000000..d6a484d8b --- /dev/null +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityTokenGenerator.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import java.security.SecureRandom; + +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.stereotype.Service; + +/** + * A security token generator service which can be used to generate security + * tokens for e.g. target or gateway tokens which are valid for authenticates + * against SP. + * + * + * + * + */ +@Service +public class SecurityTokenGenerator { + + private static final boolean LETTERS_GENERATION = true; + private static final boolean NUMBER_GENERATION = true; + private static final int TOKEN_LENGTH = 32; + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + /** + * Generates a random secure token of length {@link #TOKEN_LENGTH} + * characters with alphanumeric characters {@code A-Z_a-z_0-9}. + * + * @return a new generated random alphanumeric string. + */ + public String generateToken() { + return RandomStringUtils.random(TOKEN_LENGTH, 0, 0, LETTERS_GENERATION, NUMBER_GENERATION, null, SECURE_RANDOM); + } +} diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SpringSecurityAuditorAware.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SpringSecurityAuditorAware.java new file mode 100755 index 000000000..2d225c1dc --- /dev/null +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SpringSecurityAuditorAware.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import org.springframework.data.domain.AuditorAware; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +/** + * Auditor class that allows {@link BaseEntity}s to insert currenlt logged in + * user for repository changes. + * + * + * + * + * + */ +public class SpringSecurityAuditorAware implements AuditorAware { + + @Override + public String getCurrentAuditor() { + + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + return null; + } + + if (authentication.getPrincipal() != null) { + if (authentication.getPrincipal() instanceof UserDetails) { + return ((UserDetails) authentication.getPrincipal()).getUsername(); + } + return authentication.getPrincipal().toString(); + } + return null; + } +} diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/util/IpUtil.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/util/IpUtil.java new file mode 100755 index 000000000..0068fd0c8 --- /dev/null +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/util/IpUtil.java @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.util; + +import java.lang.annotation.Target; +import java.net.URI; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; + +import com.google.common.net.HttpHeaders; + +/** + * A utility which determines the correct IP of a connected {@link Target}. E.g + * from a {@link HttpServletRequest}. + * + * + * + * + */ +public final class IpUtil { + + private static final String SCHEME_SEPERATOR = "://"; + private static final String HTTP_SCHEME = "http"; + private static final String AMPQP_SCHEME = "amqp"; + private static final Pattern IPV4_ADDRESS_PATTERN = Pattern + .compile("([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})"); + + private static final Pattern IPV6_ADDRESS_PATTERN = Pattern.compile("([0-9a-f]{1,4}:){7}([0-9a-f]){1,4}"); + + private IpUtil() { + + } + + /** + * Retrieves the string based IP address from a given + * {@link HttpServletRequest} by either the + * {@link HttpHeaders#X_FORWARDED_FOR} or by the + * {@link HttpServletRequest#getRemoteAddr()} methods. + * + * @param request + * the {@link HttpServletRequest} to determine the IP address + * where this request has been sent from + * @param forwardHeader + * the header name containing the IP address e.g. forwarded by a + * proxy {@code x-forwarded-for} + * @return the {@link URI} based IP address from the client which sent the + * request + */ + public static URI getClientIpFromRequest(final HttpServletRequest request, final String forwardHeader) { + String ip = request.getHeader(forwardHeader); + if (ip == null || (ip = findClientIpAddress(ip)) == null) { + ip = request.getRemoteAddr(); + } + return createHttpUri(ip); + } + + private static String findClientIpAddress(final String s) { + + final Matcher matcherv4 = IPV4_ADDRESS_PATTERN.matcher(s); + + if (matcherv4.find()) { + return matcherv4.group(0); + } + + final Matcher matcherv6 = IPV6_ADDRESS_PATTERN.matcher(s); + + if (matcherv6.find()) { + return matcherv6.group(0); + } + + return null; + } + + /** + * Create a {@link URI} with scheme and host. + * + * @param scheme + * the scheme + * @param host + * the host + * @return the {@link URI} + * @throws IllegalArgumentException + * If the given string not parsable + */ + public static URI createUri(final String scheme, final String host) { + final boolean isIpV6 = host.indexOf(':') >= 0 && host.charAt(0) != '['; + if (isIpV6) { + return URI.create(scheme + SCHEME_SEPERATOR + "[" + host + "]"); + } + + return URI.create(scheme + SCHEME_SEPERATOR + host); + } + + /** + * Create a {@link URI} with amqp scheme and host. + * + * @param host + * the host + * @return the {@link URI} + * @throws IllegalArgumentException + * If the given string not parsable + */ + public static URI createAmqpUri(final String host) { + return createUri(AMPQP_SCHEME, host); + } + + /** + * Create a {@link URI} with http scheme and host. + * + * @param host + * the host + * @return the {@link URI} + * @throws IllegalArgumentException + * If the given string not parsable + */ + public static URI createHttpUri(final String host) { + return createUri(HTTP_SCHEME, host); + } + + /** + * Check if scheme contains http and uri ist not null. + * + * @param uri + * the uri + * @return true = is http host false = not + */ + public static boolean isHttpUri(final URI uri) { + return uri != null && HTTP_SCHEME.equals(uri.getScheme()); + } + + /** + * Check if host scheme amqp and uri ist not null. + * + * @param uri + * the uri + * @return true = is http host false = not + */ + public static boolean isAmqpUri(final URI uri) { + return uri != null && AMPQP_SCHEME.equals(uri.getScheme()); + } +} diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/util/SPInfo.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/util/SPInfo.java new file mode 100755 index 000000000..3a2a696df --- /dev/null +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/util/SPInfo.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.util; + +import javax.servlet.MultipartConfigElement; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +/** + * Bean which contains all informations about the SP software, e.g. like + * version, built time etc. from the environment. + * + * + * + * + */ +@Component +public class SPInfo implements EnvironmentAware { + + // package private for testing purposes + static final String UNKNOWN_VERSION = "unknown"; + + static final String UNKNOWN_CREDENTIAL = "unknown credential"; + + private Environment environmentData; + + @Autowired + private MultipartConfigElement configElement; + + /* + * (non-Javadoc) + * + * @see org.springframework.context.EnvironmentAware#setEnvironment(org. + * springframework.core.env. Environment) + */ + @Override + public void setEnvironment(final Environment environment) { + this.environmentData = environment; + } + + /** + * @return the version in string format, e.g. 1.0.0 or {@code "UNKNOWN"} in + * case the SP version info cannot be determined. + */ + public String getVersion() { + if (environmentData != null) { + return environmentData.getProperty("info.build.version", UNKNOWN_VERSION); + } + return UNKNOWN_VERSION; + } + + public String getSupportEmail() { + if (environmentData != null) { + return environmentData.getProperty("hawkbit.server.email.support"); + } + return ""; + } + + public String getRequestAccountEmail() { + if (environmentData != null) { + return environmentData.getProperty("hawkbit.server.email.request.account"); + } + return ""; + } + + public String getDemoTenant() { + if (environmentData != null) { + return environmentData.getProperty("hawkbit.server.demo.tenant"); + } + return UNKNOWN_CREDENTIAL; + } + + public String getDemoUser() { + if (environmentData != null) { + return environmentData.getProperty("hawkbit.server.demo.user"); + } + return UNKNOWN_CREDENTIAL; + + } + + public String getDemoPassword() { + if (environmentData != null) { + return environmentData.getProperty("hawkbit.server.demo.password"); + } + return UNKNOWN_CREDENTIAL; + + } + + /** + * @return the max file size to upload artifact files in bytes which has + * been configured. + */ + public long getMaxArtifactFileSize() { + return configElement.getMaxFileSize(); + } +} diff --git a/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/ExcludePathAwareShallowETagFilterTest.java b/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/ExcludePathAwareShallowETagFilterTest.java new file mode 100755 index 000000000..ce663fbfb --- /dev/null +++ b/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/ExcludePathAwareShallowETagFilterTest.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit; + +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Mockito.mockingDetails; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ExcludePathAwareShallowETagFilterTest { + + @Mock + private HttpServletRequest servletRequestMock; + + @Mock + private HttpServletResponse servletResponseMock; + + @Mock + private FilterChain filterChainMock; + + @Test + public void excludePathDoesNotCalculateETag() throws ServletException, IOException { + final String knownContextPath = "/bumlux/test"; + final String knownUri = knownContextPath + "/exclude/download"; + final String antPathExclusion = "/exclude/**"; + + // mock + when(servletRequestMock.getContextPath()).thenReturn(knownContextPath); + when(servletRequestMock.getRequestURI()).thenReturn(knownUri); + + final ExcludePathAwareShallowETagFilter filterUnderTest = new ExcludePathAwareShallowETagFilter( + antPathExclusion); + + filterUnderTest.doFilterInternal(servletRequestMock, servletResponseMock, filterChainMock); + + // verify no eTag header is set and response has not been changed + assertThat(servletResponseMock.getHeader("ETag")).isNull(); + // the servlet response must be the same mock! + verify(filterChainMock, times(1)).doFilter(servletRequestMock, servletResponseMock); + } + + @Test + public void pathNotExcludedETagIsCalculated() throws ServletException, IOException { + final String knownContextPath = "/bumlux/test"; + final String knownUri = knownContextPath + "/include/download"; + final String antPathExclusion = "/exclude/**"; + + // mock + when(servletRequestMock.getContextPath()).thenReturn(knownContextPath); + when(servletRequestMock.getRequestURI()).thenReturn(knownUri); + + final ExcludePathAwareShallowETagFilter filterUnderTest = new ExcludePathAwareShallowETagFilter( + antPathExclusion); + + final ArgumentCaptor responseArgumentCaptor = ArgumentCaptor + .forClass(HttpServletResponse.class); + + filterUnderTest.doFilterInternal(servletRequestMock, servletResponseMock, filterChainMock); + + // the servlet response must be the same mock! + verify(filterChainMock, times(1)).doFilter(Mockito.eq(servletRequestMock), responseArgumentCaptor.capture()); + assertThat(mockingDetails(responseArgumentCaptor.getValue()).isMock()).isFalse(); + } +} diff --git a/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/security/SecurityTokenGeneratorTest.java b/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/security/SecurityTokenGeneratorTest.java new file mode 100755 index 000000000..405d6d010 --- /dev/null +++ b/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/security/SecurityTokenGeneratorTest.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; + +import org.junit.Test; + +public class SecurityTokenGeneratorTest { + + @Test + public void test() throws NoSuchAlgorithmException, UnsupportedEncodingException { + final SecurityTokenGenerator securityTokenGenerator = new SecurityTokenGenerator(); + for (int index = 0; index < 1; index++) { + System.out.println(securityTokenGenerator.generateToken()); + } + } + +} diff --git a/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/util/IpUtilTest.java b/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/util/IpUtilTest.java new file mode 100755 index 000000000..e1e809ab8 --- /dev/null +++ b/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/util/IpUtilTest.java @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.util; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.net.URI; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import com.google.common.net.HttpHeaders; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +@RunWith(MockitoJUnitRunner.class) +@Features("IpUtil Test") +@Stories("Tests the created uris") +public class IpUtilTest { + + @Mock + private HttpServletRequest requestMock; + + @Test + @Description("Tests create uri from request") + public void getRemoteAddrFromRequestIfForwaredHeaderNotPresent() { + // known values + final URI knownRemoteClientIP = IpUtil.createHttpUri("127.0.0.1"); + // mock + when(requestMock.getHeader(HttpHeaders.X_FORWARDED_FOR)).thenReturn(null); + when(requestMock.getRemoteAddr()).thenReturn(knownRemoteClientIP.getHost()); + + // test + final URI remoteAddr = IpUtil.getClientIpFromRequest(requestMock, "bumlux"); + + // verify + assertThat(remoteAddr).isEqualTo(knownRemoteClientIP); + verify(requestMock, times(1)).getHeader("bumlux"); + verify(requestMock, times(1)).getRemoteAddr(); + } + + @Test + @Description("Tests create uri from x forward header") + public void getRemoteAddrFromXForwardedForHeader() { + // known values + final URI knownRemoteClientIP = IpUtil.createHttpUri("10.99.99.1"); + // mock + when(requestMock.getHeader(HttpHeaders.X_FORWARDED_FOR)).thenReturn(knownRemoteClientIP.getHost()); + when(requestMock.getRemoteAddr()).thenReturn(null); + + // test + final URI remoteAddr = IpUtil.getClientIpFromRequest(requestMock, "X-Forwarded-For"); + + // verify + assertThat(remoteAddr).isEqualTo(knownRemoteClientIP); + verify(requestMock, times(1)).getHeader(HttpHeaders.X_FORWARDED_FOR); + verify(requestMock, times(0)).getRemoteAddr(); + } + + @Test + @Description("Tests create http uri ipv4 and ipv6") + public void testCreateHttpUri() { + final String ipv4 = "10.99.99.1"; + URI httpUri = IpUtil.createHttpUri(ipv4); + assertHttpUri(ipv4, httpUri); + + final String host = "myhost"; + httpUri = IpUtil.createHttpUri(host); + assertHttpUri(host, httpUri); + + final String ipv6 = "0:0:0:0:0:0:0:1"; + httpUri = IpUtil.createHttpUri(ipv6); + assertHttpUri("[" + ipv6 + "]", httpUri); + + } + + private void assertHttpUri(final String host, final URI httpUri) { + assertTrue(IpUtil.isHttpUri(httpUri)); + assertFalse(IpUtil.isAmqpUri(httpUri)); + assertEquals(host, httpUri.getHost()); + assertEquals("http", httpUri.getScheme()); + } + + @Test + @Description("Tests create amqp uri ipv4 and ipv6") + public void testCreateAmqpUri() { + final String ipv4 = "10.99.99.1"; + URI amqpUri = IpUtil.createAmqpUri(ipv4); + assertAmqpUri(ipv4, amqpUri); + + final String host = "myhost"; + amqpUri = IpUtil.createAmqpUri(host); + assertAmqpUri(host, amqpUri); + + final String ipv6 = "0:0:0:0:0:0:0:1"; + amqpUri = IpUtil.createAmqpUri(ipv6); + assertAmqpUri("[" + ipv6 + "]", amqpUri); + } + + private void assertAmqpUri(final String host, final URI httpUri) { + assertTrue(IpUtil.isAmqpUri(httpUri)); + assertFalse(IpUtil.isHttpUri(httpUri)); + assertEquals(host, httpUri.getHost()); + assertEquals("amqp", httpUri.getScheme()); + } + + @Test(expected = IllegalArgumentException.class) + @Description("Tests create invalid uri") + public void testCreateInvalidUri() { + final String host = "10.99.99.1"; + final URI testUri = IpUtil.createUri("test", host); + assertFalse(IpUtil.isAmqpUri(testUri)); + assertFalse(IpUtil.isHttpUri(testUri)); + assertEquals(host, testUri.getHost()); + IpUtil.createUri(":/", host); + fail(); + + } +} diff --git a/hawkbit-security-integration/pom.xml b/hawkbit-security-integration/pom.xml new file mode 100755 index 000000000..6f5ce10a7 --- /dev/null +++ b/hawkbit-security-integration/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + org.eclipse.hawkbit + hawkbit-parent + 0.1.0-SNAPSHOT + + hawkbit-security-integration + hawkBit :: Security Integration + + + + + org.eclipse.hawkbit + hawkbit-repository + ${project.version} + + + org.eclipse.hawkbit + hawkbit-dmf-api + ${project.version} + + + org.springframework.security + spring-security-web + + + + + diff --git a/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/AbstractControllerAuthenticationFilter.java b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/AbstractControllerAuthenticationFilter.java new file mode 100755 index 000000000..bbffc8748 --- /dev/null +++ b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/AbstractControllerAuthenticationFilter.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import org.eclipse.hawkbit.dmf.json.model.TenantSecruityToken; +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An abstraction for all controller based security. Check if the tenant + * configuration is enabled. + * + * + * + */ +public abstract class AbstractControllerAuthenticationFilter implements PreAuthenficationFilter { + + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractControllerAuthenticationFilter.class); + + protected final SystemManagement systemManagement; + protected final TenantAware tenantAware; + private final SecurityConfigurationKeyTenantRunner configurationKeyTenantRunner; + + protected AbstractControllerAuthenticationFilter(final SystemManagement systemManagement, + final TenantAware tenantAware) { + this.systemManagement = systemManagement; + this.tenantAware = tenantAware; + this.configurationKeyTenantRunner = new SecurityConfigurationKeyTenantRunner(); + } + + protected abstract TenantConfigurationKey getTenantConfigurationKey(); + + @Override + public boolean isEnable(final TenantSecruityToken secruityToken) { + return tenantAware.runAsTenant(secruityToken.getTenant(), configurationKeyTenantRunner); + } + + @Override + public abstract HeaderAuthentication getPreAuthenticatedPrincipal(TenantSecruityToken secruityToken); + + @Override + public abstract HeaderAuthentication getPreAuthenticatedCredentials(TenantSecruityToken secruityToken); + + private final class SecurityConfigurationKeyTenantRunner implements TenantAware.TenantRunner { + @Override + public Boolean run() { + LOGGER.trace("retrieving configuration value for configuration key {}", getTenantConfigurationKey()); + return systemManagement.getConfigurationValue(getTenantConfigurationKey(), Boolean.class); + } + + } +} diff --git a/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/CoapAnonymousPreAuthenticatedFilter.java b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/CoapAnonymousPreAuthenticatedFilter.java new file mode 100755 index 000000000..d9d8b9fa7 --- /dev/null +++ b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/CoapAnonymousPreAuthenticatedFilter.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import org.eclipse.hawkbit.dmf.json.model.TenantSecruityToken; + +/** + * A Filter for device which download via coap. + * + * + * + */ +public class CoapAnonymousPreAuthenticatedFilter implements PreAuthenficationFilter { + + @Override + public HeaderAuthentication getPreAuthenticatedPrincipal(final TenantSecruityToken secruityToken) { + return new HeaderAuthentication(secruityToken.getControllerId(), TenantSecruityToken.COAP_TOKEN_VALUE); + } + + @Override + public HeaderAuthentication getPreAuthenticatedCredentials(final TenantSecruityToken secruityToken) { + return new HeaderAuthentication(secruityToken.getControllerId(), TenantSecruityToken.COAP_TOKEN_VALUE); + } + + @Override + public boolean isEnable(final TenantSecruityToken secruityToken) { + final String authHeader = secruityToken.getHeader(TenantSecruityToken.COAP_AUTHORIZATION_HEADER); + return TenantSecruityToken.COAP_TOKEN_VALUE.equals(authHeader); + } + +} diff --git a/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/ControllerPreAuthenticateSecurityTokenFilter.java b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/ControllerPreAuthenticateSecurityTokenFilter.java new file mode 100755 index 000000000..3cf09cff8 --- /dev/null +++ b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/ControllerPreAuthenticateSecurityTokenFilter.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import org.eclipse.hawkbit.dmf.json.model.TenantSecruityToken; +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails; +import org.eclipse.hawkbit.repository.ControllerManagement; +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; + +/** + * An pre-authenticated processing filter which extracts (if enabled through + * configuration) the possibility to authenticate a target based on its target + * security-token with the {@code Authorization} HTTP header. + * {@code Example Header: Authorization: TargetToken + * 5d8fSD54fdsFG98DDsa.} + * + * + * + */ +public class ControllerPreAuthenticateSecurityTokenFilter extends AbstractControllerAuthenticationFilter { + + private static final Logger LOGGER = LoggerFactory.getLogger(ControllerPreAuthenticateSecurityTokenFilter.class); + private static final String TARGET_SECURITY_TOKEN_AUTH_SCHEME = "TargetToken "; + private static final int OFFSET_TARGET_TOKEN = TARGET_SECURITY_TOKEN_AUTH_SCHEME.length(); + + private final ControllerManagement controllerManagement; + + /** + * Constructor. + * + * @param systemManagement + * the system management service to retrieve configuration + * properties + * @param controllerManagement + * the controller management to retrieve the specific target + * security token to verify + * @param tenantAware + * the tenant aware service to get configuration for the specific + * tenant + */ + public ControllerPreAuthenticateSecurityTokenFilter(final SystemManagement systemManagement, + final ControllerManagement controllerManagement, final TenantAware tenantAware) { + super(systemManagement, tenantAware); + this.controllerManagement = controllerManagement; + } + + @Override + public HeaderAuthentication getPreAuthenticatedPrincipal(final TenantSecruityToken secruityToken) { + final String authHeader = secruityToken.getHeader(TenantSecruityToken.AUTHORIZATION_HEADER); + if ((authHeader != null) && authHeader.startsWith(TARGET_SECURITY_TOKEN_AUTH_SCHEME)) { + LOGGER.debug("found authorization header with scheme {} using target security token for authentication", + TARGET_SECURITY_TOKEN_AUTH_SCHEME); + return new HeaderAuthentication(secruityToken.getControllerId(), authHeader.substring(OFFSET_TARGET_TOKEN)); + } + LOGGER.debug( + "security token filter is enabled but requst does not contain either the necessary path variables {} or the authorization header with scheme {}", + secruityToken, TARGET_SECURITY_TOKEN_AUTH_SCHEME); + return null; + } + + @Override + public HeaderAuthentication getPreAuthenticatedCredentials(final TenantSecruityToken secruityToken) { + final String securityToken = tenantAware.runAsTenant(secruityToken.getTenant(), + new GetSecurityTokenTenantRunner(secruityToken.getTenant(), secruityToken.getControllerId())); + return new HeaderAuthentication(secruityToken.getControllerId(), securityToken); + } + + @Override + protected TenantConfigurationKey getTenantConfigurationKey() { + return TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED; + } + + private final class GetSecurityTokenTenantRunner implements TenantAware.TenantRunner { + + private final String controllerId; + private final String tenant; + + private GetSecurityTokenTenantRunner(final String tenant, final String controllerId) { + this.tenant = tenant; + this.controllerId = controllerId; + } + + @Override + public String run() { + LOGGER.trace("retrieving security token for controllerId {}", controllerId); + final SecurityContext oldContext = SecurityContextHolder.getContext(); + try { + SecurityContextHolder.setContext(getSecurityTokenReadContext()); + return controllerManagement.getSecurityTokenByControllerId(controllerId); + } finally { + SecurityContextHolder.setContext(oldContext); + } + } + + private SecurityContext getSecurityTokenReadContext() { + final SecurityContextImpl securityContextImpl = new SecurityContextImpl(); + securityContextImpl.setAuthentication(getSecurityTokenReadAuthentication()); + return securityContextImpl; + } + + private Authentication getSecurityTokenReadAuthentication() { + final AnonymousAuthenticationToken anonymousAuthenticationToken = new AnonymousAuthenticationToken( + "anonymous-read-security-token", "anonymous", com.google.common.collect.Lists + .newArrayList(new SimpleGrantedAuthority(SpPermission.READ_TARGET_SEC_TOKEN))); + anonymousAuthenticationToken.setDetails(new TenantAwareAuthenticationDetails(tenant, true)); + return anonymousAuthenticationToken; + } + } +} diff --git a/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/ControllerPreAuthenticatedGatewaySecurityTokenFilter.java b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/ControllerPreAuthenticatedGatewaySecurityTokenFilter.java new file mode 100755 index 000000000..7e8848ed8 --- /dev/null +++ b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/ControllerPreAuthenticatedGatewaySecurityTokenFilter.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import org.eclipse.hawkbit.dmf.json.model.TenantSecruityToken; +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An pre-authenticated processing filter which extracts (if enabled through + * configuration) the possibility to authenticate a target based through a + * gateway security token. This is commonly used for targets connected + * indirectly via a gateway. This gateway controls multiple targets under the + * gateway security token which can be set via the {@code TenantSecruityToken} + * header. {@code Example Header: Authorization: GatewayToken + * 5d8fSD54fdsFG98DDsa.} + * + * + * + */ +public class ControllerPreAuthenticatedGatewaySecurityTokenFilter extends AbstractControllerAuthenticationFilter { + + private static final Logger LOGGER = LoggerFactory + .getLogger(ControllerPreAuthenticatedGatewaySecurityTokenFilter.class); + private static final String GATEWAY_SECURITY_TOKEN_AUTH_SCHEME = "GatewayToken "; + private static final int OFFSET_GATEWAY_TOKEN = GATEWAY_SECURITY_TOKEN_AUTH_SCHEME.length(); + + private final GetGatewaySecurityConfigurationKeyTenantRunner gatewaySecurityTokenKeyConfigRunner = new GetGatewaySecurityConfigurationKeyTenantRunner(); + + /** + * Constructor. + * + * @param systemManagement + * the system management service to retrieve configuration + * properties + * @param tenantAware + * the tenant aware service to get configuration for the specific + * tenant + */ + public ControllerPreAuthenticatedGatewaySecurityTokenFilter(final SystemManagement systemManagement, + final TenantAware tenantAware) { + super(systemManagement, tenantAware); + } + + @Override + public HeaderAuthentication getPreAuthenticatedPrincipal(final TenantSecruityToken secruityToken) { + final String authHeader = secruityToken.getHeader(TenantSecruityToken.AUTHORIZATION_HEADER); + if ((authHeader != null) && authHeader.startsWith(GATEWAY_SECURITY_TOKEN_AUTH_SCHEME)) { + LOGGER.debug("found authorization header with scheme {} using target security token for authentication", + GATEWAY_SECURITY_TOKEN_AUTH_SCHEME); + return new HeaderAuthentication(secruityToken.getControllerId(), + authHeader.substring(OFFSET_GATEWAY_TOKEN)); + } + LOGGER.debug( + "security token filter is enabled but request does not contain either the necessary secruity token {} or the authorization header with scheme {}", + secruityToken, GATEWAY_SECURITY_TOKEN_AUTH_SCHEME); + return null; + } + + @Override + public HeaderAuthentication getPreAuthenticatedCredentials(final TenantSecruityToken secruityToken) { + final String gatewayToken = tenantAware.runAsTenant(secruityToken.getTenant(), + gatewaySecurityTokenKeyConfigRunner); + return new HeaderAuthentication(secruityToken.getControllerId(), gatewayToken); + } + + @Override + protected TenantConfigurationKey getTenantConfigurationKey() { + return TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED; + } + + private final class GetGatewaySecurityConfigurationKeyTenantRunner implements TenantAware.TenantRunner { + + @Override + public String run() { + LOGGER.trace("retrieving configuration value for configuration key {}", + TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY); + return systemManagement.getConfigurationValue( + TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class); + } + } + +} diff --git a/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/ControllerPreAuthenticatedSecurityHeaderFilter.java b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/ControllerPreAuthenticatedSecurityHeaderFilter.java new file mode 100755 index 000000000..3e4b39949 --- /dev/null +++ b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/ControllerPreAuthenticatedSecurityHeaderFilter.java @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import org.eclipse.hawkbit.dmf.json.model.TenantSecruityToken; +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An pre-authenticated processing filter which extracts the principal from a + * request URI and the credential from a request header in a the + * {@link TenantSecruityToken}. + * + * + * + */ +public class ControllerPreAuthenticatedSecurityHeaderFilter extends AbstractControllerAuthenticationFilter { + + private static final Logger LOGGER = LoggerFactory.getLogger(ControllerPreAuthenticatedSecurityHeaderFilter.class); + private static final Logger LOG_SECURITY_AUTH = LoggerFactory.getLogger("server-security.authentication"); + + private final GetSecurityAuthorityNameTenantRunner sslIssuerNameConfigTenantRunner = new GetSecurityAuthorityNameTenantRunner(); + // Example Headers with Cert Information + // Clientip: 217.24.201.180 + // X-Forwarded-Proto: https + // X-Ssl-Client-Cn: my.name + // X-Ssl-Client-Dn: + // CN=my.name,CN=O,CN=R,CN=DE,CN=BOSCH,CN=pki,DC=bosch,DC=com + // X-Ssl-Client-Hash: 7f:87:cb:b5:9c:e0:c5:0a:1a:a6:57:69:0f:ca:0a:95 + // X-Ssl-Client-Notafter: Dec 18 08:02:45 2017 GMT + // X-Ssl-Client-Notbefore: Dec 18 07:32:45 2014 GMT + // X-Ssl-Client-Verify: ok + // X-Ssl-Issuer: CN=Bosch-CA1-DE,CN=PKI,DC=Bosch,DC=com + // X-Ssl-Issuer-Dn-1: CN=Bosch-CA-DE,CN=PKI,DC=Bosch,DC=com + // X-Ssl-Issuer-Hash-1: ae:11:f5:6a:0a:e8:74:50:81:0e:0c:37:ec:c5:22:fc + private final String caCommonNameHeader; + // the X-Ssl-Issuer-Hash basic header header which contains the x509 + // fingerprint hash, this + // header exists multiple in the request for all trusted chain. + private final String sslIssuerHashBasicHeader; + + /** + * Constructor. + * + * @param caCommonNameHeader + * the http-header which holds the common-name of the certificate + * @param caAuthorityNameHeader + * the http-header which holds the ca-authority name of the + * certificate + * @param systemManagement + * the system management service to retrieve configuration + * properties to check if the header authentication is enabled + * for this tenant + * @param tenantAware + * the tenant aware service to get configuration for the specific + * tenant + */ + public ControllerPreAuthenticatedSecurityHeaderFilter(final String caCommonNameHeader, + final String caAuthorityNameHeader, final SystemManagement systemManagement, + final TenantAware tenantAware) { + super(systemManagement, tenantAware); + this.caCommonNameHeader = caCommonNameHeader; + this.sslIssuerHashBasicHeader = caAuthorityNameHeader; + } + + @Override + public HeaderAuthentication getPreAuthenticatedPrincipal(final TenantSecruityToken secruityToken) { + // retrieve the common name header and the authority name header from + // the http request and + // combine them together + final String commonNameValue = secruityToken.getHeader(caCommonNameHeader); + final String knownSslIssuerConfigurationValue = tenantAware.runAsTenant(secruityToken.getTenant(), + sslIssuerNameConfigTenantRunner); + final String sslIssuerHashValue = getIssuerHashHeader(secruityToken, knownSslIssuerConfigurationValue); + if (commonNameValue != null && LOGGER.isTraceEnabled()) { + LOGGER.trace("Found commonNameHeader {}={}, using as credentials", caCommonNameHeader, commonNameValue); + } + if (sslIssuerHashValue != null && LOGGER.isTraceEnabled()) { + LOGGER.trace("Found sslIssuerHash ****, using as credentials for tenant {}", secruityToken.getTenant()); + } + + if (commonNameValue != null && sslIssuerHashValue != null) { + return new HeaderAuthentication(commonNameValue, sslIssuerHashValue); + } + return null; + } + + @Override + public HeaderAuthentication getPreAuthenticatedCredentials(final TenantSecruityToken secruityToken) { + final String authorityNameConfigurationValue = tenantAware.runAsTenant(secruityToken.getTenant(), + sslIssuerNameConfigTenantRunner); + String controllerId = secruityToken.getControllerId(); + // in case of legacy download artifact, the controller ID is not in the + // URL path, so then + // we just use the common name header + if (controllerId == null || "anonymous".equals(controllerId)) { + controllerId = secruityToken.getHeader(caCommonNameHeader); + } + + return new HeaderAuthentication(controllerId, authorityNameConfigurationValue); + } + + /** + * Iterates over the {@link #sslIssuerHashBasicHeader} basic header + * {@code X-Ssl-Issuer-Hash-%d} and try to finds the same hash as known. + * It's ok if we find the the hash in any the trusted CA chain to accept + * this request for this tenant. + */ + private String getIssuerHashHeader(final TenantSecruityToken secruityToken, final String knownIssuerHash) { + // iterate over the headers until we get a null header. + int iHeader = 1; + String foundHash = null; + while ((foundHash = secruityToken.getHeader(String.format(sslIssuerHashBasicHeader, iHeader))) != null) { + if (foundHash.equals(knownIssuerHash)) { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Found matching ssl issuer hash at position {}", iHeader); + } + return foundHash; + } + iHeader++; + } + LOG_SECURITY_AUTH.debug( + "Certifacte request but no matching hash found in headers {} for common name {} in request", + sslIssuerHashBasicHeader, secruityToken.getHeader(caCommonNameHeader)); + return null; + } + + @Override + protected TenantConfigurationKey getTenantConfigurationKey() { + return TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED; + } + + private final class GetSecurityAuthorityNameTenantRunner implements TenantAware.TenantRunner { + @Override + public String run() { + return systemManagement.getConfigurationValue( + TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class); + } + } +} diff --git a/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/HeaderAuthentication.java b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/HeaderAuthentication.java new file mode 100755 index 000000000..ca840c689 --- /dev/null +++ b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/HeaderAuthentication.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +/** + * The authentication principal and credentials object which holds the + * controller-id and the authority name from the http-headers as principal or + * from the http-url and tenant configuration for the credentials. + * + * + * + */ +public final class HeaderAuthentication { + private final String controllerId; + private final String headerAuth; + + HeaderAuthentication(final String controllerId, final String headerAuth) { + this.controllerId = controllerId; + this.headerAuth = headerAuth; + } + + @Override + public int hashCode() {// NOSONAR - as this is generated + final int prime = 31; + int result = 1; + result = prime * result + ((controllerId == null) ? 0 : controllerId.hashCode()); + result = prime * result + ((headerAuth == null) ? 0 : headerAuth.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) {// NOSONAR - as this is generated + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final HeaderAuthentication other = (HeaderAuthentication) obj; + if (controllerId == null) { + if (other.controllerId != null) { + return false; + } + } else if (!controllerId.equals(other.controllerId)) { + return false; + } + if (headerAuth == null) { + if (other.headerAuth != null) { + return false; + } + } else if (!headerAuth.equals(other.headerAuth)) { + return false; + } + return true; + } + + @Override + public String toString() { + // only the controller ID because the principal is stored as string for + // audit information + // etc. + return controllerId; + } + +} diff --git a/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/PreAuthTokenSourceTrustAuthenticationProvider.java b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/PreAuthTokenSourceTrustAuthenticationProvider.java new file mode 100755 index 000000000..ca38404f7 --- /dev/null +++ b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/PreAuthTokenSourceTrustAuthenticationProvider.java @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +/** + * An spring authentication provider which supportes authentication tokens of + * type {@link PreAuthenticatedAuthenticationToken} created by the + * {@link ControllerPreAuthenticatedSecurityHeaderFilter}. + * + * Addtionally to the authentication token providing the principal and the + * credentials which must be match, this authentication provider can also check + * the remote IP address of the request. + * + * E.g. The request path is /controller/v1/{controllerId} then the controllerId + * in the path is the principal. The credentials are the extracted information + * from e.g. a certificate provided by an reverse proxy. Due this request is + * only allowed from a specific source address this authentication manager can + * also check the remote IP address of the request. + * + * + * + */ +public class PreAuthTokenSourceTrustAuthenticationProvider implements AuthenticationProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(PreAuthenticatedAuthenticationToken.class); + private final List authorizedSourceIps; + + /** + * Creates a new PreAuthTokenSourceTrustAuthenticationProvider without + * source IPs, which disables the source IP check. + */ + public PreAuthTokenSourceTrustAuthenticationProvider() { + authorizedSourceIps = null; + } + + /** + * Creates a new PreAuthTokenSourceTrustAuthenticationProvider with given + * source IP addresses which are trusted and should be checked against the + * request remote IP address. + * + * @param authorizedSourceIps + * a list of IP addresses. + */ + public PreAuthTokenSourceTrustAuthenticationProvider(final List authorizedSourceIps) { + this.authorizedSourceIps = authorizedSourceIps; + } + + /** + * Creates a new PreAuthTokenSourceTrustAuthenticationProvider with given + * source IP addresses which are trusted and should be checked against the + * request remote IP address. + * + * @param authorizedSourceIps + * a list of IP addresses. + */ + public PreAuthTokenSourceTrustAuthenticationProvider(final String... authorizedSourceIps) { + this.authorizedSourceIps = new ArrayList<>(); + for (final String ip : authorizedSourceIps) { + this.authorizedSourceIps.add(ip); + } + } + + @Override + public Authentication authenticate(final Authentication authentication) { + if (!supports(authentication.getClass())) { + return null; + } + + boolean successAuthentication = false; + final PreAuthenticatedAuthenticationToken token = (PreAuthenticatedAuthenticationToken) authentication; + final Object credentials = token.getCredentials(); + final Object principal = token.getPrincipal(); + final Object tokenDetails = token.getDetails(); + + if (principal == null) { + throw new BadCredentialsException("The provided principal and credentials are not match"); + } + + // check if principal equals credentials because we want to check if + // e.g. controllerId + // containing in the URL equals the controllerId in the special header + // set by the reverse + // proxy which extracted the CN from the certificate + if (principal.equals(credentials)) { + successAuthentication = checkSourceIPAddressIfNeccessary(tokenDetails); + } + + if (successAuthentication) { + final Collection controllerAuthorities = new ArrayList<>(); + controllerAuthorities.add(new SimpleGrantedAuthority(SpringEvalExpressions.CONTROLLER_ROLE)); + final PreAuthenticatedAuthenticationToken successToken = new PreAuthenticatedAuthenticationToken(principal, + credentials, controllerAuthorities); + successToken.setDetails(tokenDetails); + return successToken; + } + + throw new BadCredentialsException("The provided principal and credentials are not match"); + } + + private boolean checkSourceIPAddressIfNeccessary(final Object tokenDetails) { + boolean success = authorizedSourceIps == null; + String remoteAddress = null; + // controllerIds in URL path and request header are the same but is the + // request coming + // from a trustful source, like the reverse proxy. + if (authorizedSourceIps != null) { + if (!(tokenDetails instanceof TenantAwareWebAuthenticationDetails)) { + // is not of type WebAuthenticationDetails, then we cannot + // determine the remote address! + LOGGER.error( + "Cannot determine the controller remote-ip-address based on the given authentication token - {} , token details are not TenantAwareWebAuthenticationDetails! ", + tokenDetails); + success = false; + } else { + remoteAddress = ((TenantAwareWebAuthenticationDetails) tokenDetails).getRemoteAddress(); + if (authorizedSourceIps.contains(remoteAddress)) { + // source ip matches the given pattern -> authenticated + success = true; + } + } + } + + if (!success) { + throw new InsufficientAuthenticationException("The remote source IP address " + remoteAddress + + " is not in the list of trusted IP addresses " + authorizedSourceIps); + } + + // no trusted IP check, because no authorizedSourceIPs configuration + return true; + } + + @Override + public boolean supports(final Class authentication) { + return PreAuthenticatedAuthenticationToken.class.isAssignableFrom(authentication); + } + +} diff --git a/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/PreAuthenficationFilter.java b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/PreAuthenficationFilter.java new file mode 100755 index 000000000..8aa957005 --- /dev/null +++ b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/PreAuthenficationFilter.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import org.eclipse.hawkbit.dmf.json.model.TenantSecruityToken; + +/** + * Interface for Pre Authenfication. + * + * + * + */ +public interface PreAuthenficationFilter { + + /** + * Check if the filter is enabled. + * + * @param secruityToken + * the secruity info + * @return is enabled diabled + */ + boolean isEnable(TenantSecruityToken secruityToken); + + /** + * Extract the principal information from the current secruityToken. + * + * @param secruityToken + * the secruityToken + * @return the extracted tenant and controller id + */ + HeaderAuthentication getPreAuthenticatedPrincipal(TenantSecruityToken secruityToken); + + /** + * Extract the principal credentials from the current secruityToken. + * + * @param secruityToken + * the secruityToken + * @return the extracted tenant and controller id + */ + HeaderAuthentication getPreAuthenticatedCredentials(TenantSecruityToken secruityToken); + +} diff --git a/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/TenantAwareWebAuthenticationDetails.java b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/TenantAwareWebAuthenticationDetails.java new file mode 100755 index 000000000..2a6fbd31a --- /dev/null +++ b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/TenantAwareWebAuthenticationDetails.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails; + +/** + * Extends the {@link TenantAwareAuthenticationDetails} to web information to + * retrieve also e.g. the remoteAddress of the {@link HttpServletRequest} when + * authenticating the requested controller e.g. based on the security header and + * trusted IP address we need the remote address of the http request to verify + * the e.g. the reverse proxy is trusted and allowed to set the header. + * + * + * + */ +public class TenantAwareWebAuthenticationDetails extends TenantAwareAuthenticationDetails { + + private static final long serialVersionUID = 1L; + private final String remoteAddress; + + /** + * @param tenant + * the current tenant + * @param remoteAddress + * the remote address of this web request + * @param controller + * {@code true} indicates this is an controller HTTP request + * otherwise {@code false}. + */ + public TenantAwareWebAuthenticationDetails(final String tenant, final String remoteAddress, + final boolean controller) { + super(tenant, controller); + this.remoteAddress = remoteAddress; + } + + /** + * @return the remoteAddress + */ + public String getRemoteAddress() { + return remoteAddress; + } +} diff --git a/hawkbit-test-report/placeholder.txt b/hawkbit-test-report/placeholder.txt new file mode 100755 index 000000000..fe9171ff8 --- /dev/null +++ b/hawkbit-test-report/placeholder.txt @@ -0,0 +1,10 @@ +==== + Copyright (c) 2015 Bosch Software Innovations GmbH and others. + + All rights reserved. This program and the accompanying materials + are made available under the terms of the Eclipse Public License v1.0 + which accompanies this distribution, and is available at + http://www.eclipse.org/legal/epl-v10.html +==== + +A zip need 1 file \ No newline at end of file diff --git a/hawkbit-test-report/pom.xml b/hawkbit-test-report/pom.xml new file mode 100755 index 000000000..b6eb54a0a --- /dev/null +++ b/hawkbit-test-report/pom.xml @@ -0,0 +1,82 @@ + + + + 4.0.0 + + org.eclipse.hawkbit + hawkbit-parent + 0.1.0-SNAPSHOT + + hawkbit-test-report + Hawkbit :: Test Report + + pom + + + + + maven-resources-plugin + 2.7 + + + copy-resources + verify + + copy-resources + + + ${basedir}/target/allure-results + + + ${basedir}/.. + false + + **/target/allure-results/*.xml + + + + ${basedir} + false + + placeholder.txt + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.5.3 + + + src/main/assembly/test-report.xml + + false + + + + create-report-zip-assembly + verify + + single + + + + + + + + \ No newline at end of file diff --git a/hawkbit-test-report/src/main/assembly/test-report.xml b/hawkbit-test-report/src/main/assembly/test-report.xml new file mode 100755 index 000000000..c4689825c --- /dev/null +++ b/hawkbit-test-report/src/main/assembly/test-report.xml @@ -0,0 +1,25 @@ + + + testReport + / + false + + zip + + + + ${project.build.directory}/allure-results + / + + + diff --git a/hawkbit-ui/.gitignore b/hawkbit-ui/.gitignore new file mode 100755 index 000000000..81311d80e --- /dev/null +++ b/hawkbit-ui/.gitignore @@ -0,0 +1 @@ +/database/ diff --git a/hawkbit-ui/pom.xml b/hawkbit-ui/pom.xml new file mode 100755 index 000000000..0f88a208b --- /dev/null +++ b/hawkbit-ui/pom.xml @@ -0,0 +1,243 @@ + + + 4.0.0 + + org.eclipse.hawkbit + hawkbit-parent + 0.1.0-SNAPSHOT + + hawkbit-ui + hawkBit :: UI + Bosch IoT Software Provisioning server web application + + + + com.vaadin + vaadin-maven-plugin + ${vaadin.plugin.version} + + -Xmx1g -Xss1024k + + ${project.build.directory}/classes/VAADIN/widgetsets + ${project.build.directory}/classes/VAADIN/widgetsets + true + + false + ${project.build.directory}/gwtdirt + true + + true + + ${project.build.directory}/gwt-deploy + + + + + + + + + clean + resources + update-theme + update-widgetset + compile-theme + compile + + + + + + net.alchim31.maven + yuicompressor-maven-plugin + 1.5.0 + + + + compress + + + + + + **/*.css + + true + ${basedir}/src/main/resources/VAADIN/themes/hawkbit/styles.css + true + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + + org.codehaus.mojo + + + properties-maven-plugin + + + [1.0-alpha-2,) + + + + write-project-properties + + + + + + + + + + com.vaadin + vaadin-maven-plugin + [7.5.2,) + + compile-theme + + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + [1.7,) + + run + + + + + + + + + + + + + + + + + + org.apache.logging.log4j + log4j-api + + + org.eclipse.hawkbit + hawkbit-repository + ${project.version} + + + + com.vaadin + vaadin-spring-boot + + + org.vaadin.spring.addons + vaadin-spring-addon-eventbus + + + org.vaadin.spring.extensions + vaadin-spring-ext-security + + + com.vaadin + vaadin-server + + + + com.vaadin + vaadin-push + + + org.springframework.security + spring-security-web + + + + com.vaadin + vaadin-themes + + + org.vaadin.addons.lazyquerycontainer + vaadin-lazyquerycontainer + + + org.eclipse.persistence + javax.persistence + + + com.vaadin + vaadin-client-compiled + + + com.vaadin + vaadin-themes + + + + + org.vaadin.addons + flexibleoptiongroup + + + org.vaadin.addons + tokenfield + + + + + org.mockito + mockito-core + test + + + org.easytesting + fest-assert-core + test + + + org.easytesting + fest-assert + test + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + test + + + ru.yandex.qatools.allure + allure-junit-adaptor + test + + + \ No newline at end of file diff --git a/hawkbit-ui/src/main/.gitignore b/hawkbit-ui/src/main/.gitignore new file mode 100755 index 000000000..263ecccb6 --- /dev/null +++ b/hawkbit-ui/src/main/.gitignore @@ -0,0 +1 @@ +/webapp/ diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/push/AsyncVaadinServletConfiguration.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/push/AsyncVaadinServletConfiguration.java new file mode 100755 index 000000000..6eb6761e6 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/push/AsyncVaadinServletConfiguration.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.push; + +import org.atmosphere.container.JSR356AsyncSupport; +import org.atmosphere.cpr.ApplicationConfig; +import org.springframework.boot.context.embedded.ServletRegistrationBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import com.vaadin.spring.boot.internal.VaadinServletConfiguration; +import com.vaadin.spring.boot.internal.VaadinServletConfigurationProperties; + +/** + * {@link VaadinServletConfiguration} that sets the context path for + * {@link JSR356AsyncSupport} that registers + * {@link SpringSecurityAtmosphereInterceptor} for spring security integration. + * + * + * + */ +@Configuration +@EnableConfigurationProperties(VaadinServletConfigurationProperties.class) +@Import(VaadinServletConfiguration.class) +public class AsyncVaadinServletConfiguration extends VaadinServletConfiguration { + + @Bean + protected ServletRegistrationBean vaadinServletRegistration() { + return createServletRegistrationBean(); + } + + @Override + protected void addInitParameters(final ServletRegistrationBean servletRegistrationBean) { + super.addInitParameters(servletRegistrationBean); + + servletRegistrationBean.addInitParameter(ApplicationConfig.JSR356_MAPPING_PATH, "/UI"); + servletRegistrationBean.addInitParameter(ApplicationConfig.ATMOSPHERE_INTERCEPTORS, + SpringSecurityAtmosphereInterceptor.class.getName()); + + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/push/SpringSecurityAtmosphereInterceptor.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/push/SpringSecurityAtmosphereInterceptor.java new file mode 100755 index 000000000..64423bd4f --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/push/SpringSecurityAtmosphereInterceptor.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.push; + +import org.atmosphere.config.service.AtmosphereInterceptorService; +import org.atmosphere.cpr.Action; +import org.atmosphere.cpr.AtmosphereInterceptor; +import org.atmosphere.cpr.AtmosphereInterceptorAdapter; +import org.atmosphere.cpr.AtmosphereResource; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; + +/** + * An {@link AtmosphereInterceptor} implementation which retrieves the + * {@link SecurityContext} from the http-session and set in into the + * {@link SecurityContextHolder}. This is necessary due that websocket requests + * are not going through the spring security filter chain and the + * {@link SecurityContext} will not be present in the current Thread. + * + * + * + * + */ +@AtmosphereInterceptorService +public class SpringSecurityAtmosphereInterceptor extends AtmosphereInterceptorAdapter { + + /* + * (non-Javadoc) + * + * @see org.atmosphere.cpr.AtmosphereInterceptor#inspect(org.atmosphere.cpr. + * AtmosphereResource) + */ + @Override + public Action inspect(final AtmosphereResource r) { + final SecurityContext context = (SecurityContext) r.getRequest().getSession() + .getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); + SecurityContextHolder.setContext(context); + return Action.CONTINUE; + } + + @Override + public void postInspect(final AtmosphereResource r) { + SecurityContextHolder.clearContext(); + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/repository/OffsetBasedPageRequest.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/repository/OffsetBasedPageRequest.java new file mode 100755 index 000000000..f463bf7ab --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/repository/OffsetBasedPageRequest.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; + +/** + * An implementation of the {@link PageRequest} which is offset based by means + * the offset is given and not the page number as in the original + * {@link PageRequest} implemntation where the offset is generated. Due that the + * REST-API is working with {@code offset} and {@code limit} parameter we need + * an offset based page request for JPA. + * + * + * + * + */ +public final class OffsetBasedPageRequest extends PageRequest { + + private static final long serialVersionUID = 1L; + private final int offset; + + /** + * Creates a new {@link OffsetBasedPageRequest}. Offsets are zero indexed, + * thus providing 0 for {@code offset} will return the first entry. + * + * @param offset + * zero-based offset index. + * @param limit + * the limit of the page to be returned. + */ + public OffsetBasedPageRequest(final int offset, final int limit) { + this(offset, limit, null); + } + + /** + * Creates a new {@link OffsetBasedPageRequest}. Offsets are zero indexed, + * thus providing 0 for {@code offset} will return the first entry. + * + * @param offset + * zero-based offset index. + * @param limit + * the limit of the page to be returned. + * @param sort + * sort can be {@literal null}. + */ + public OffsetBasedPageRequest(final int offset, final int limit, final Sort sort) { + super(0, limit, sort); + this.offset = offset; + } + + @Override + public int getOffset() { + return offset; + } + + @Override + public String toString() { + return "OffsetBasedPageRequest [offset=" + offset + ", getPageSize()=" + getPageSize() + ", getPageNumber()=" + + getPageNumber() + "]"; + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/repository/SpPermissionChecker.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/repository/SpPermissionChecker.java new file mode 100755 index 000000000..0df7af872 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/repository/SpPermissionChecker.java @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.io.Serializable; + +import org.eclipse.hawkbit.im.authentication.PermissionService; +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +/** + * Bean which contains all SP permissions. + * + * + * + * + */ +@Validated +@Service +public class SpPermissionChecker implements Serializable { + private static final long serialVersionUID = 2757865286212875704L; + + @Autowired + private transient PermissionService permissionService; + + /** + * Gets the SP monitor View Permission. + * + * @return SYSTEM_MONITOR boolean value + */ + public boolean hasSpMonitorViewPermission() { + return permissionService.hasPermission(SpPermission.SYSTEM_MONITOR); + } + + /** + * Gets the SP diagnosis retrieval Permission. + * + * @return SYSTEM_DIAG boolean value + */ + public boolean hasSpdiagnosisViewPermission() { + return permissionService.hasPermission(SpPermission.SYSTEM_DIAG); + } + + /** + * Gets the SP administration retrieval Permission. + * + * @return SYSTEM_ADMIN boolean value + */ + public boolean hasSpAdminViewPermission() { + return permissionService.hasPermission(SpPermission.SYSTEM_ADMIN); + } + + /** + * Gets the SP read Target & Dist Permission. + * + * @return TARGET_REPOSITORY_READ boolean value + */ + public boolean hasTargetAndRepositoryReadPermission() { + return hasTargetReadPermission() && hasReadDistributionPermission(); + } + + /** + * Gets the SP read Target Permission. + * + * @return READ_TARGET boolean value + */ + public boolean hasTargetReadPermission() { + return permissionService.hasPermission(SpPermission.READ_TARGET); + } + + /** + * Gets the SP create Target Permission. + * + * @return READ_TARGET boolean value + */ + public boolean hasCreateTargetPermission() { + return hasTargetReadPermission() && permissionService.hasPermission(SpPermission.CREATE_TARGET); + } + + /** + * Gets the SP update Target Permission. + * + * @return READ_TARGET boolean value + */ + public boolean hasUpdateTargetPermission() { + return hasTargetReadPermission() && permissionService.hasPermission(SpPermission.UPDATE_TARGET); + } + + /** + * Gets the SP delete Target Permission. + * + * @return READ_TARGET boolean value + */ + public boolean hasDeleteTargetPermission() { + return hasTargetReadPermission() && permissionService.hasPermission(SpPermission.DELETE_TARGET); + } + + /** + * Gets the SP READ Distribution Permission. + * + * @return READ_REPOSITORY boolean value + */ + public boolean hasReadDistributionPermission() { + return permissionService.hasPermission(SpPermission.READ_REPOSITORY); + } + + /** + * Gets the SP create Distribution Permission. + * + * @return CREATE_REPOSITORY boolean value + */ + public boolean hasCreateDistributionPermission() { + return hasReadDistributionPermission() && permissionService.hasPermission(SpPermission.CREATE_REPOSITORY); + } + + /** + * Gets the SP update Distribution Permission. + * + * @return UPDATE_REPOSITORY boolean value + */ + public boolean hasUpdateDistributionPermission() { + return hasReadDistributionPermission() && permissionService.hasPermission(SpPermission.UPDATE_REPOSITORY); + } + + /** + * Gets the SP delete Distribution Permission. + * + * @return DELETE_REPOSITORY boolean value + */ + public boolean hasDeleteDistributionPermission() { + return hasReadDistributionPermission() && permissionService.hasPermission(SpPermission.DELETE_REPOSITORY); + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/AppWidgetSet.gwt.xml b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/AppWidgetSet.gwt.xml new file mode 100755 index 000000000..8811defaf --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/AppWidgetSet.gwt.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/DefaultHawkbitUI.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/DefaultHawkbitUI.java new file mode 100755 index 000000000..959df4c43 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/DefaultHawkbitUI.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui; + +import org.eclipse.hawkbit.ui.themes.HawkbitTheme; + +import com.vaadin.annotations.Theme; +import com.vaadin.annotations.Widgetset; +import com.vaadin.ui.UI; + +/** + * Abstract class for the ui to set widgetset and theme. + * + * + * + * + */ +@SuppressWarnings("serial") +@Widgetset(value = HawkbitTheme.WIDGET_SET_NAME) +@Theme(HawkbitTheme.THEME_NAME) +public abstract class DefaultHawkbitUI extends UI { + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/DispatcherRunnable.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/DispatcherRunnable.java new file mode 100755 index 000000000..69620347a --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/DispatcherRunnable.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui; + +import org.eclipse.hawkbit.eventbus.event.Event; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.vaadin.spring.events.EventBus; + +import com.vaadin.server.VaadinSession; +import com.vaadin.ui.UI; +import com.vaadin.util.CurrentInstance; + +/** + * A {@link Runnable} implementation for the {@link UI#access(Runnable)} to + * dispatch events to the UI in the UI thread. + * + * + * + * + */ +public class DispatcherRunnable implements Runnable { + + private final SecurityContext userContext; + private final Event event; + private final VaadinSession session; + private final EventBus eventBus; + + /** + * @param eventBus + * the event bus to distribute the event to + * @param session + * the current Vaadin session + * @param userContext + * the context of the currently logged in user to distribute the + * event to + * @param event + * the event which is distributed to the UI. + */ + public DispatcherRunnable(final EventBus eventBus, final VaadinSession session, final SecurityContext userContext, + final Event event) { + this.eventBus = eventBus; + this.session = session; + this.userContext = userContext; + this.event = event; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Runnable#run() + */ + @Override + public void run() { + CurrentInstance.setCurrent(session.getUIs().iterator().next()); + CurrentInstance.setCurrent(session); + SecurityContextHolder.setContext(userContext); + eventBus.publish(this, event); + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/ErrorView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/ErrorView.java new file mode 100755 index 000000000..4e4cb05a6 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/ErrorView.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui; + +import org.eclipse.hawkbit.ui.menu.DashboardMenu; +import org.eclipse.hawkbit.ui.menu.DashboardMenuItem; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; +import org.springframework.beans.factory.annotation.Autowired; + +import com.vaadin.navigator.Navigator; +import com.vaadin.navigator.View; +import com.vaadin.navigator.ViewChangeListener; +import com.vaadin.shared.Position; +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.UIScope; +import com.vaadin.ui.Label; +import com.vaadin.ui.Notification; +import com.vaadin.ui.Notification.Type; +import com.vaadin.ui.UI; +import com.vaadin.ui.VerticalLayout; + +/** + * View class that is instantiated when no other view matches the navigation + * state. + * + * + * + * @see Navigator#setErrorView(Class) + * + */ +@SuppressWarnings("serial") +@SpringComponent +@UIScope +class ErrorView extends VerticalLayout implements View { + + private final Label message; + + @Autowired + private I18N i18n; + + @Autowired + private DashboardMenu dashboardMenu; + + /** + * Constructor. + */ + public ErrorView() { + setMargin(true); + message = new Label(); + addComponent(message); + } + + @Override + public void enter(final ViewChangeListener.ViewChangeEvent event) { + final DashboardMenuItem view = dashboardMenu.getByViewName(event.getViewName()); + if (view == null) { + message.setValue(i18n.get("message.error.view", new Object[] { event.getViewName() })); + return; + } + if (dashboardMenu.isAccessDenied(event.getViewName())) { + final Notification nt = new Notification("Access denied", + i18n.get("message.accessdenied.view", new Object[] { event.getViewName() }), Type.ERROR_MESSAGE, + false); + nt.setStyleName(SPUILabelDefinitions.SP_NOTIFICATION_ERROR_MESSAGE_STYLE); + nt.setPosition(Position.BOTTOM_RIGHT); + nt.show(UI.getCurrent().getPage()); + message.setValue(i18n.get("message.accessdenied.view", new Object[] { event.getViewName() })); + } + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/HawkbitUI.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/HawkbitUI.java new file mode 100755 index 000000000..967f82121 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/HawkbitUI.java @@ -0,0 +1,293 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui; + +import java.io.IOException; +import java.util.Locale; +import java.util.Set; + +import javax.servlet.http.Cookie; + +import org.eclipse.hawkbit.eventbus.event.EntityEvent; +import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails; +import org.eclipse.hawkbit.ui.components.SPUIErrorHandler; +import org.eclipse.hawkbit.ui.menu.DashboardEvent.PostViewChangeEvent; +import org.eclipse.hawkbit.ui.menu.DashboardMenu; +import org.eclipse.hawkbit.ui.menu.DashboardMenuItem; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; +import org.eclipse.hawkbit.ui.utils.SpringContextHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.vaadin.spring.events.EventBus; +import org.vaadin.spring.events.EventBus.SessionEventBus; + +import com.google.common.eventbus.AllowConcurrentEvents; +import com.google.common.eventbus.Subscribe; +import com.vaadin.annotations.Title; +import com.vaadin.navigator.Navigator; +import com.vaadin.navigator.View; +import com.vaadin.navigator.ViewChangeListener; +import com.vaadin.navigator.ViewProvider; +import com.vaadin.server.ClientConnector.DetachListener; +import com.vaadin.server.Responsive; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinService; +import com.vaadin.server.VaadinSession; +import com.vaadin.server.VaadinSession.State; +import com.vaadin.server.WrappedSession; +import com.vaadin.spring.navigator.SpringViewProvider; +import com.vaadin.ui.Component; +import com.vaadin.ui.CssLayout; +import com.vaadin.ui.CustomLayout; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.UI; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.themes.ValoTheme; + +/** + * Vaadin management UI. + * + * + * + * + */ +@SuppressWarnings("serial") +@Title("hawkBit Update Server") +public class HawkbitUI extends DefaultHawkbitUI implements DetachListener { + private static final Logger LOG = LoggerFactory.getLogger(HawkbitUI.class); + + private static final String EMPTY_VIEW = ""; + + @Autowired + private SpringViewProvider viewProvider; + + @Autowired + private ApplicationContext context; + + @Autowired + private I18N i18n; + + @Autowired + private DashboardMenu dashboardMenu; + + private HorizontalLayout content; + + @Autowired + private ErrorView errorview; + + @Autowired + protected EventBus.SessionEventBus eventBus; + + /** + * An {@link com.google.common.eventbus.EventBus} subscriber which + * subscribes {@link EntityEvent} from the repository to dispatch these + * events to the UI {@link SessionEventBus}. + * + * @param event + * the entity event which has been published from the repository + */ + @Subscribe + @AllowConcurrentEvents + public void dispatch(final org.eclipse.hawkbit.eventbus.event.Event event) { + final VaadinSession session = getSession(); + if (session != null && session.getState() == State.OPEN) { + final WrappedSession wrappedSession = session.getSession(); + if (wrappedSession != null) { + final SecurityContext userContext = (SecurityContext) wrappedSession + .getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); + if (eventSecurityCheck(userContext, event)) { + final SecurityContext oldContext = SecurityContextHolder.getContext(); + try { + access(new DispatcherRunnable(eventBus, session, userContext, event)); + } finally { + SecurityContextHolder.setContext(oldContext); + } + } + } + } + } + + protected boolean eventSecurityCheck(final SecurityContext userContext, + final org.eclipse.hawkbit.eventbus.event.Event event) { + if (userContext != null && userContext.getAuthentication() != null) { + final Object tenantAuthenticationDetails = userContext.getAuthentication().getDetails(); + if (tenantAuthenticationDetails instanceof TenantAwareAuthenticationDetails) { + return ((TenantAwareAuthenticationDetails) tenantAuthenticationDetails).getTenant().equalsIgnoreCase( + event.getTenant()); + } + } + return false; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.server.ClientConnector.DetachListener#detach(com.vaadin.server + * .ClientConnector. DetachEvent) + */ + @Override + public void detach(final DetachEvent event) { + LOG.info("ManagementUI is detached uiid - {}", getUIId()); + + } + + @Override + protected void init(final VaadinRequest vaadinRequest) { + LOG.info("ManagementUI init starts uiid - {}", getUI().getUIId()); + addDetachListener(this); + SpringContextHelper.setContext(context); + + Responsive.makeResponsive(this); + addStyleName(ValoTheme.UI_WITH_MENU); + setResponsive(Boolean.TRUE); + + final HorizontalLayout rootLayout = new HorizontalLayout(); + rootLayout.setSizeFull(); + + dashboardMenu.init(); + dashboardMenu.setResponsive(Boolean.TRUE); + + final VerticalLayout contentVerticalLayout = new VerticalLayout(); + contentVerticalLayout.addComponent(buildHeader()); + contentVerticalLayout.setSizeFull(); + + rootLayout.addComponent(dashboardMenu); + rootLayout.addComponent(contentVerticalLayout); + + content = new HorizontalLayout(); + contentVerticalLayout.addComponent(content); + content.setStyleName("view-content"); + content.setSizeFull(); + rootLayout.setExpandRatio(contentVerticalLayout, 1.0f); + contentVerticalLayout.setStyleName("main-content"); + contentVerticalLayout.setExpandRatio(content, 1.0F); + setContent(rootLayout); + final Resource resource = context.getResource("classpath:/VAADIN/themes/" + UI.getCurrent().getTheme() + + "/layouts/footer.html"); + try { + final CustomLayout customLayout = new CustomLayout(resource.getInputStream()); + customLayout.setSizeUndefined(); + contentVerticalLayout.addComponent(customLayout); + } catch (final IOException ex) { + LOG.error("Footer file is missing", ex); + } + final Navigator navigator = new Navigator(this, content); + navigator.addViewChangeListener(new ViewChangeListener() { + @Override + public boolean beforeViewChange(final ViewChangeEvent event) { + return true; + } + + @Override + public void afterViewChange(final ViewChangeEvent event) { + final DashboardMenuItem view = dashboardMenu.getByViewName(event.getViewName()); + dashboardMenu.postViewChange(new PostViewChangeEvent(view)); + if (view == null) { + content.setCaption(null); + return; + } + content.setCaption(view.getDashboardCaptionLong()); + } + }); + + navigator.setErrorView(errorview); + navigator.addProvider(new ManagementViewProvider()); + setNavigator(navigator); + navigator.addView(EMPTY_VIEW, new Navigator.EmptyView()); + // set locale is required for I18N class also, to get the locale from + // cookie + final String locale = getLocaleId(SPUIDefinitions.AVAILABLE_LOCALES); + setLocale(new Locale(locale)); + + UI.getCurrent().setErrorHandler(new SPUIErrorHandler()); + LOG.info("Current locale of the application is : {}", i18n.getLocale()); + } + + private Component buildHeader() { + final CssLayout cssLayout = new CssLayout(); + cssLayout.setStyleName("view-header"); + return cssLayout; + } + + /** + * Get Specific Locale. + * + * @param availableLocalesInApp + * as set + * @return String as preferred locale + */ + private String getLocaleId(final Set availableLocalesInApp) { + final String[] localeChain = getLocaleChain(); + String spLocale = SPUIDefinitions.DEFAULT_LOCALE; + if (null != localeChain) { + // Find best matching locale + for (final String localeId : localeChain) { + if (availableLocalesInApp.contains(localeId)) { + spLocale = localeId; + break; + } + } + } + return spLocale; + } + + /** + * Get Locale for i18n. + * + * @return String as locales + */ + private String[] getLocaleChain() { + String[] localeChain = null; + // Fetch all cookies from the request + final Cookie[] cookies = VaadinService.getCurrentRequest().getCookies(); + if (cookies != null) { + for (final Cookie c : cookies) { + if (c.getName().equals(SPUIDefinitions.COOKIE_NAME) && !c.getValue().isEmpty()) { + localeChain = c.getValue().split("#"); + break; + } + } + } + return localeChain; + } + + private class ManagementViewProvider implements ViewProvider { + + @Override + public String getViewName(final String viewAndParameters) { + return viewProvider.getViewName(getStartView(viewAndParameters)); + } + + @Override + public View getView(final String viewName) { + return viewProvider.getView(getStartView(viewName)); + } + + private String getStartView(final String viewName) { + final DashboardMenuItem view = dashboardMenu.getByViewName(viewName); + if ("".equals(viewName) && !dashboardMenu.isAccessibleViewsEmpty()) { + return dashboardMenu.getInitialViewName(); + } + if (view == null || dashboardMenu.isAccessDenied(viewName)) { + return " "; + } + return viewName; + } + + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/UploadArtifactView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/UploadArtifactView.java new file mode 100755 index 000000000..54ba5cc8e --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/UploadArtifactView.java @@ -0,0 +1,273 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts; + +import javax.annotation.PreDestroy; + +import org.eclipse.hawkbit.repository.SpPermissionChecker; +import org.eclipse.hawkbit.ui.HawkbitUI; +import org.eclipse.hawkbit.ui.artifacts.details.ArtifactDetailsLayout; +import org.eclipse.hawkbit.ui.artifacts.event.ArtifactDetailsEvent; +import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleEvent; +import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleEvent.SoftwareModuleEventType; +import org.eclipse.hawkbit.ui.artifacts.footer.SMDeleteActionsLayout; +import org.eclipse.hawkbit.ui.artifacts.smtable.SoftwareModuleTableLayout; +import org.eclipse.hawkbit.ui.artifacts.smtype.SMTypeFilterLayout; +import org.eclipse.hawkbit.ui.artifacts.state.ArtifactUploadState; +import org.eclipse.hawkbit.ui.artifacts.upload.UploadLayout; +import org.eclipse.hawkbit.ui.management.event.DragEvent; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; +import org.eclipse.hawkbit.ui.utils.UINotification; +import org.springframework.beans.factory.annotation.Autowired; +import org.vaadin.spring.events.EventBus; +import org.vaadin.spring.events.EventScope; +import org.vaadin.spring.events.annotation.EventBusListenerMethod; + +import com.vaadin.event.MouseEvents.ClickListener; +import com.vaadin.navigator.View; +import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent; +import com.vaadin.server.Page; +import com.vaadin.server.Page.BrowserWindowResizeEvent; +import com.vaadin.server.Page.BrowserWindowResizeListener; +import com.vaadin.spring.annotation.SpringView; +import com.vaadin.spring.annotation.ViewScope; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.DragAndDropWrapper; +import com.vaadin.ui.GridLayout; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.UI; +import com.vaadin.ui.VerticalLayout; + +/** + * Display artifacts upload view. + * + * + */ +@SpringView(name = UploadArtifactView.VIEW_NAME, ui = HawkbitUI.class) +@ViewScope +public class UploadArtifactView extends VerticalLayout implements View, BrowserWindowResizeListener { + + public static final String VIEW_NAME = "spUpload"; + private static final long serialVersionUID = 8754632011301553682L; + + @Autowired + private transient EventBus.SessionEventBus eventBus; + + @Autowired + private SpPermissionChecker permChecker; + + @Autowired + private I18N i18n; + + @Autowired + private transient UINotification uiNotification; + + @Autowired + private ArtifactUploadState artifactUploadState; + + @Autowired + private SMTypeFilterLayout filterByTypeLayout; + + @Autowired + private SoftwareModuleTableLayout smTableLayout; + + @Autowired + private ArtifactDetailsLayout artifactDetailsLayout; + + @Autowired + private UploadLayout uploadLayout; + + @Autowired + private SMDeleteActionsLayout deleteActionsLayout; + + private VerticalLayout detailAndUploadLayout; + + private HorizontalLayout uplaodButtonsLayout; + + private GridLayout mainLayout; + private DragAndDropWrapper dadw; + + @Override + public void enter(final ViewChangeEvent event) { + buildLayout(); + restoreState(); + checkNoDataAvaialble(); + eventBus.subscribe(this); + Page.getCurrent().addBrowserWindowResizeListener(this); + showOrHideFilterButtons(Page.getCurrent().getBrowserWindowWidth()); + } + + @PreDestroy + void destroy() { + eventBus.unsubscribe(this); + } + + @EventBusListenerMethod(scope = EventScope.SESSION) + void onEvent(final SoftwareModuleEvent event) { + if (event.getSoftwareModuleEventType() == SoftwareModuleEventType.MINIMIZED) { + minimizeSwTable(); + } else if (event.getSoftwareModuleEventType() == SoftwareModuleEventType.MAXIMIZED) { + maximizeSwTable(); + } + } + + @EventBusListenerMethod(scope = EventScope.SESSION) + void onEvent(final ArtifactDetailsEvent event) { + if (event == ArtifactDetailsEvent.MINIMIZED) { + minimizeArtifactoryDetails(); + } else if (event == ArtifactDetailsEvent.MAXIMIZED) { + maximizeArtifactoryDetails(); + } + } + + private void restoreState() { + if (artifactUploadState.isSwModuleTableMaximized()) { + maximizeSwTable(); + } + if (artifactUploadState.isArtifactDetailsMaximized()) { + maximizeArtifactoryDetails(); + } + } + + private void buildLayout() { + if (permChecker.hasReadDistributionPermission() || permChecker.hasCreateDistributionPermission()) { + setSizeFull(); + createMainLayout(); + addComponents(mainLayout); + setExpandRatio(mainLayout, 1); + hideDropHints(); + } + } + + private VerticalLayout createDetailsAndUploadLayout() { + detailAndUploadLayout = new VerticalLayout(); + + detailAndUploadLayout.addComponent(artifactDetailsLayout); + detailAndUploadLayout.setComponentAlignment(artifactDetailsLayout, Alignment.MIDDLE_CENTER); + + if (permChecker.hasCreateDistributionPermission()) { + dadw = uploadLayout.getDropAreaWrapper(); + detailAndUploadLayout.addComponent(dadw); + detailAndUploadLayout.setComponentAlignment(dadw, Alignment.MIDDLE_CENTER); + } + + detailAndUploadLayout.setExpandRatio(artifactDetailsLayout, 1.0f); + detailAndUploadLayout.setSizeFull(); + detailAndUploadLayout.addStyleName("group"); + detailAndUploadLayout.setSpacing(true); + return detailAndUploadLayout; + + } + + private GridLayout createMainLayout() { + createDetailsAndUploadLayout(); + createUploadButtonLayout(); + mainLayout = new GridLayout(3, 2); + mainLayout.setSizeFull(); + mainLayout.setSpacing(true); + mainLayout.addComponent(filterByTypeLayout, 0, 0); + mainLayout.addComponent(smTableLayout, 1, 0); + mainLayout.addComponent(detailAndUploadLayout, 2, 0); + mainLayout.addComponent(deleteActionsLayout, 1, 1); + mainLayout.addComponent(uplaodButtonsLayout, 2, 1); + mainLayout.setRowExpandRatio(0, 1.0f); + mainLayout.setColumnExpandRatio(1, 0.5f); + mainLayout.setColumnExpandRatio(2, 0.5f); + mainLayout.setComponentAlignment(deleteActionsLayout, Alignment.BOTTOM_CENTER); + mainLayout.setComponentAlignment(uplaodButtonsLayout, Alignment.BOTTOM_CENTER); + return mainLayout; + } + + private void createUploadButtonLayout() { + uplaodButtonsLayout = new HorizontalLayout(); + if (permChecker.hasCreateDistributionPermission()) { + uplaodButtonsLayout = uploadLayout.getFileUploadLayout(); + } + + } + + private void minimizeSwTable() { + mainLayout.addComponent(detailAndUploadLayout, 2, 0); + addOtherComponents(); + } + + private void maximizeSwTable() { + mainLayout.removeComponent(detailAndUploadLayout); + removeOtherComponents(); + mainLayout.setColumnExpandRatio(1, 1f); + mainLayout.setColumnExpandRatio(2, 0f); + } + + private void minimizeArtifactoryDetails() { + mainLayout.setSpacing(true); + detailAndUploadLayout.addComponent(dadw); + mainLayout.addComponent(filterByTypeLayout, 0, 0); + mainLayout.addComponent(smTableLayout, 1, 0); + addOtherComponents(); + } + + private void maximizeArtifactoryDetails() { + mainLayout.setSpacing(false); + mainLayout.removeComponent(filterByTypeLayout); + mainLayout.removeComponent(smTableLayout); + detailAndUploadLayout.removeComponent(dadw); + removeOtherComponents(); + mainLayout.setColumnExpandRatio(1, 0f); + mainLayout.setColumnExpandRatio(2, 1f); + } + + private void addOtherComponents() { + mainLayout.addComponent(deleteActionsLayout, 1, 1); + mainLayout.addComponent(uplaodButtonsLayout, 2, 1); + mainLayout.setColumnExpandRatio(1, 0.5f); + mainLayout.setColumnExpandRatio(2, 0.5f); + mainLayout.setComponentAlignment(deleteActionsLayout, Alignment.BOTTOM_CENTER); + mainLayout.setComponentAlignment(uplaodButtonsLayout, Alignment.BOTTOM_CENTER); + } + + private void removeOtherComponents() { + mainLayout.removeComponent(deleteActionsLayout); + mainLayout.removeComponent(uplaodButtonsLayout); + + } + + private void hideDropHints() { + UI.getCurrent().addClickListener(new ClickListener() { + @Override + public void click(final com.vaadin.event.MouseEvents.ClickEvent event) { + eventBus.publish(this, DragEvent.HIDE_DROP_HINT); + } + }); + } + + private void checkNoDataAvaialble() { + if (artifactUploadState.isNoDataAvilableSoftwareModule()) { + + uiNotification.displayValidationError(i18n.get("message.no.data")); + } + } + + @Override + public void browserWindowResized(final BrowserWindowResizeEvent event) { + showOrHideFilterButtons(event.getWidth()); + } + + private void showOrHideFilterButtons(final int browserWidth) { + if (browserWidth < SPUIDefinitions.REQ_MIN_BROWSER_WIDTH) { + + filterByTypeLayout.setVisible(false); + smTableLayout.setShowFilterButtonVisible(true); + } else if (!artifactUploadState.isSwTypeFilterClosed()) { + filterByTypeLayout.setVisible(true); + smTableLayout.setShowFilterButtonVisible(false); + } + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/UploadArtifactViewMenuItem.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/UploadArtifactViewMenuItem.java new file mode 100755 index 000000000..7f31f75b1 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/UploadArtifactViewMenuItem.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.ui.menu.DashboardMenuItem; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import com.vaadin.server.FontAwesome; +import com.vaadin.server.Resource; + +/** + * Display artifacts upload view menu item. + * + * + */ +@Component +@Order(400) +public class UploadArtifactViewMenuItem implements DashboardMenuItem { + + private static final long serialVersionUID = 4096851897640769726L; + + @Override + public String getViewName() { + return UploadArtifactView.VIEW_NAME; + } + + @Override + public Resource getDashboardIcon() { + return FontAwesome.UPLOAD; + } + + @Override + public String getDashboardCaption() { + return "Upload"; + } + + @Override + public String getDashboardCaptionLong() { + return "Upload Management"; + } + + @Override + public List getPermissions() { + return Arrays.asList(SpPermission.CREATE_REPOSITORY, SpPermission.READ_REPOSITORY); + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/details/ArtifactBeanQuery.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/details/ArtifactBeanQuery.java new file mode 100755 index 000000000..2fa3763fd --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/details/ArtifactBeanQuery.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.details; + +import java.util.List; +import java.util.Map; + +import org.eclipse.hawkbit.repository.ArtifactManagement; +import org.eclipse.hawkbit.repository.OffsetBasedPageRequest; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; +import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; +import org.eclipse.hawkbit.ui.utils.SpringContextHelper; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.vaadin.addons.lazyquerycontainer.AbstractBeanQuery; +import org.vaadin.addons.lazyquerycontainer.QueryDefinition; + +/** + * Simple implementation of generics bean query which dynamically loads artifact + * beans. + * + * + * + * + * + */ +public class ArtifactBeanQuery extends AbstractBeanQuery { + private static final long serialVersionUID = -333786310371208962L; + private Sort sort = new Sort(Direction.DESC, "filename"); + private transient ArtifactManagement artifactManagement = null; + private transient Page firstPagetArtifacts = null; + private Long baseSwModuleId = null; + + /** + * Parametric Constructor. + * + * @param definition + * as Def + * @param queryConfig + * as Config + * @param sortIds + * as sort + * @param sortStates + * as Sort status + */ + public ArtifactBeanQuery(final QueryDefinition definition, final Map queryConfig, + final Object[] sortIds, final boolean[] sortStates) { + super(definition, queryConfig, sortIds, sortStates); + + if (HawkbitCommonUtil.mapCheckStrKey(queryConfig)) { + baseSwModuleId = (Long) queryConfig.get(SPUIDefinitions.BY_BASE_SOFTWARE_MODULE); + } + + if (HawkbitCommonUtil.checkBolArray(sortStates)) { + // Initalize Sor + sort = new Sort(sortStates[0] ? Direction.ASC : Direction.DESC, (String) sortIds[0]); + // Add sort. + for (int targetId = 1; targetId < sortIds.length; targetId++) { + sort.and(new Sort(sortStates[targetId] ? Direction.ASC : Direction.DESC, (String) sortIds[targetId])); + } + } + } + + @Override + protected LocalArtifact constructBean() { + return new LocalArtifact(); + } + + @Override + protected List loadBeans(final int startIndex, final int count) { + Page artifactBeans; + if (startIndex == 0 && firstPagetArtifacts != null) { + artifactBeans = firstPagetArtifacts; + } else { + artifactBeans = getArtifactManagement().findLocalArtifactBySoftwareModule( + new OffsetBasedPageRequest(startIndex, count, sort), baseSwModuleId); + } + + return artifactBeans.getContent(); + } + + @Override + protected void saveBeans(final List addedTargets, final List modifiedTargets, + final List removedTargets) { + // CRUD operations on Target will be done through repository methods + } + + @Override + public int size() { + long size = 0; + if (baseSwModuleId != null) { + firstPagetArtifacts = getArtifactManagement().findLocalArtifactBySoftwareModule( + new PageRequest(0, SPUIDefinitions.PAGE_SIZE, sort), baseSwModuleId); + size = firstPagetArtifacts.getTotalElements(); + } + if (size > Integer.MAX_VALUE) { + size = Integer.MAX_VALUE; + } + + return (int) size; + } + + private ArtifactManagement getArtifactManagement() { + if (artifactManagement == null) { + artifactManagement = SpringContextHelper.getBean(ArtifactManagement.class); + } + return artifactManagement; + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/details/ArtifactDetailsLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/details/ArtifactDetailsLayout.java new file mode 100755 index 000000000..f39f76b06 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/details/ArtifactDetailsLayout.java @@ -0,0 +1,515 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.details; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.eclipse.hawkbit.repository.ArtifactManagement; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.ui.artifacts.event.ArtifactDetailsEvent; +import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleEvent; +import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleEvent.SoftwareModuleEventType; +import org.eclipse.hawkbit.ui.artifacts.state.ArtifactUploadState; +import org.eclipse.hawkbit.ui.common.ConfirmationDialog; +import org.eclipse.hawkbit.ui.components.SPUIButton; +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleSmallNoBorder; +import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPDateTimeUtil; +import org.eclipse.hawkbit.ui.utils.SPUIComponetIdProvider; +import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; +import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; +import org.eclipse.hawkbit.ui.utils.SpringContextHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.vaadin.addons.lazyquerycontainer.BeanQueryFactory; +import org.vaadin.addons.lazyquerycontainer.LazyQueryContainer; +import org.vaadin.addons.lazyquerycontainer.LazyQueryDefinition; +import org.vaadin.spring.events.EventBus; +import org.vaadin.spring.events.EventScope; +import org.vaadin.spring.events.annotation.EventBusListenerMethod; + +import com.google.common.base.Strings; +import com.vaadin.data.Container; +import com.vaadin.server.FontAwesome; +import com.vaadin.shared.ui.label.ContentMode; +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.Button; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.Table; +import com.vaadin.ui.Table.ColumnGenerator; +import com.vaadin.ui.Table.TableDragMode; +import com.vaadin.ui.UI; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.themes.ValoTheme; + +/** + * Display the details of the artifacts for a selected software module. + * + * + */ + +@SpringComponent +@ViewScope +public class ArtifactDetailsLayout extends VerticalLayout { + + private static final long serialVersionUID = -5189069028037133891L; + + private static final String PROVIDED_FILE_NAME = "filename"; + + private static final String LAST_MODIFIED_DATE = "lastModifiedAt"; + + private static final String CREATE_MODIFIED_DATE_UPLOAD = "Created/Modified Date"; + + private static final String ACTION = "action"; + + private static final String CREATED_DATE = "createdAt"; + + private static final String SIZE = "size"; + + private static final String SHA1HASH = "sha1Hash"; + + private static final String MD5HASH = "md5Hash"; + + @Autowired + private I18N i18n; + + @Autowired + private transient EventBus.SessionEventBus eventBus; + + @Autowired + private ArtifactUploadState artifactUploadState; + + private Label titleOfArtifactDetails; + + private SPUIButton maxMinButton; + + private Table artifactDetailsTable; + + private Table maxArtifactDetailsTable; + + private boolean fullWindowMode = false; + + private UI ui; + + private boolean readOnly = false; + + /** + * Initialize the artifact details layout. + */ + @PostConstruct + private void init() { + createComponents(); + buildLayout(); + eventBus.subscribe(this); + ui = UI.getCurrent(); + if (artifactUploadState.getSelectedBaseSoftwareModule().isPresent()) { + final SoftwareModule selectedSoftwareModule = artifactUploadState.getSelectedBaseSoftwareModule().get(); + populateArtifactDetails(selectedSoftwareModule.getId(), HawkbitCommonUtil + .getFormattedNameVersion(selectedSoftwareModule.getName(), selectedSoftwareModule.getVersion())); + } + if (isMaximized()) { + maximizedArtifactDetailsView(); + } + } + + /** + * Initialize the artifact details layout in readonly mode (will not show + * the delete icon) . + * + * @param readOnly + * value true for read only. + */ + public void init(final boolean readOnly) { + this.readOnly = readOnly; + init(); + } + + private void createComponents() { + String labelStr = ""; + if (artifactUploadState.getSelectedBaseSoftwareModule().isPresent()) { + final SoftwareModule softwareModule = artifactUploadState.getSelectedBaseSoftwareModule().get(); + labelStr = HawkbitCommonUtil.getFormattedNameVersion(softwareModule.getName(), softwareModule.getVersion()); + } + titleOfArtifactDetails = SPUIComponentProvider.getLabel( + HawkbitCommonUtil.getArtifactoryDetailsLabelId(labelStr), SPUILabelDefinitions.SP_WIDGET_CAPTION); + titleOfArtifactDetails.setContentMode(ContentMode.HTML); + titleOfArtifactDetails.setSizeFull(); + titleOfArtifactDetails.setImmediate(true); + maxMinButton = createMaxMinButton(); + + artifactDetailsTable = createArtifactDetailsTable(); + + artifactDetailsTable.setContainerDataSource(createArtifactLazyQueryContainer()); + addGeneratedColumn(artifactDetailsTable); + if (!readOnly) { + addGeneratedColumnButton(artifactDetailsTable); + } + setTableColumnDetails(artifactDetailsTable); + + } + + /** + * @return + */ + private SPUIButton createMaxMinButton() { + final SPUIButton button = (SPUIButton) SPUIComponentProvider.getButton(SPUIDefinitions.EXPAND_ACTION_HISTORY, + "", "", null, true, FontAwesome.EXPAND, SPUIButtonStyleSmallNoBorder.class); + button.addClickListener(event -> maxArtifactDetails()); + return button; + } + + private void buildLayout() { + + final HorizontalLayout header = new HorizontalLayout(); + header.addStyleName("artifact-details-header"); + header.addStyleName("bordered-layout"); + header.addStyleName("no-border-bottom"); + header.setSpacing(false); + header.setMargin(false); + header.setSizeFull(); + header.setHeightUndefined(); + header.setImmediate(true); + header.addComponents(titleOfArtifactDetails, maxMinButton); + header.setComponentAlignment(titleOfArtifactDetails, Alignment.TOP_LEFT); + header.setComponentAlignment(maxMinButton, Alignment.TOP_RIGHT); + header.setExpandRatio(titleOfArtifactDetails, 1.0f); + + setSizeFull(); + setImmediate(true); + addStyleName("artifact-table"); + addStyleName("table-layout"); + addComponent(header); + setComponentAlignment(header, Alignment.MIDDLE_CENTER); + addComponent(artifactDetailsTable); + setComponentAlignment(artifactDetailsTable, Alignment.MIDDLE_CENTER); + setExpandRatio(artifactDetailsTable, 1.0f); + + } + + private Container createArtifactLazyQueryContainer() { + final Map queryConfiguration = new HashMap(); + return getArtifactLazyQueryContainer(queryConfiguration); + } + + private LazyQueryContainer getArtifactLazyQueryContainer(final Map queryConfig) { + + final BeanQueryFactory artifactQF = new BeanQueryFactory( + ArtifactBeanQuery.class); + artifactQF.setQueryConfiguration(queryConfig); + final LazyQueryContainer artifactCont = new LazyQueryContainer(new LazyQueryDefinition(true, 10, "id"), + artifactQF); + addArtifactTableProperties(artifactCont); + return artifactCont; + } + + private void addArtifactTableProperties(final LazyQueryContainer artifactCont) { + artifactCont.addContainerProperty(PROVIDED_FILE_NAME, Label.class, "", false, false); + artifactCont.addContainerProperty(SIZE, Long.class, null, false, false); + artifactCont.addContainerProperty(SHA1HASH, String.class, null, false, false); + artifactCont.addContainerProperty(MD5HASH, String.class, null, false, false); + artifactCont.addContainerProperty(CREATED_DATE, Date.class, null, false, false); + artifactCont.addContainerProperty(LAST_MODIFIED_DATE, Date.class, null, false, false); + if (!readOnly) { + artifactCont.addContainerProperty(ACTION, Label.class, null, false, false); + } + } + + private void addGeneratedColumn(final Table table) { + table.addGeneratedColumn(CREATE_MODIFIED_DATE_UPLOAD, new ColumnGenerator() { + private static final long serialVersionUID = -866800417175863258L; + + @Override + public String generateCell(final Table source, final Object itemId, final Object columnId) { + final Long createdDate = (Long) table.getContainerDataSource().getItem(itemId) + .getItemProperty(CREATED_DATE).getValue(); + final Long modifiedDATE = (Long) table.getContainerDataSource().getItem(itemId) + .getItemProperty(LAST_MODIFIED_DATE).getValue(); + if (modifiedDATE != null) { + return SPDateTimeUtil.getFormattedDate(modifiedDATE); + } + return SPDateTimeUtil.getFormattedDate(createdDate); + } + }); + + } + + private void addGeneratedColumnButton(final Table table) { + table.addGeneratedColumn(ACTION, new ColumnGenerator() { + private static final long serialVersionUID = -866800417175863258L; + + @Override + public Button generateCell(final Table source, final Object itemId, final Object columnId) { + final String fileName = (String) table.getContainerDataSource().getItem(itemId) + .getItemProperty(PROVIDED_FILE_NAME).getValue(); + final Button deleteIcon = SPUIComponentProvider.getButton( + fileName + "-" + SPUIComponetIdProvider.UPLOAD_FILE_DELETE_ICON, "", + SPUILabelDefinitions.DISCARD, ValoTheme.BUTTON_TINY + " " + "redicon", true, + FontAwesome.TRASH_O, SPUIButtonStyleSmallNoBorder.class); + deleteIcon.setData(itemId); + deleteIcon.addClickListener(event -> confirmAndDeleteArtifact((Long) itemId, fileName)); + return deleteIcon; + } + }); + } + + private void confirmAndDeleteArtifact(final Long id, final String fileName) { + + final ConfirmationDialog confirmDialog = new ConfirmationDialog(i18n.get("caption.delete.artifact.confirmbox"), + i18n.get("message.delete.artifact", new Object[] { fileName }), i18n.get("button.ok"), + i18n.get("button.cancel"), ok -> { + if (ok) { + + final ArtifactManagement artifactManagement = SpringContextHelper + .getBean(ArtifactManagement.class); + artifactManagement.deleteLocalArtifact(id); + + populateArtifactDetails(artifactUploadState.getSelectedBaseSwModuleId().get(), + HawkbitCommonUtil.getFormattedNameVersion( + artifactUploadState.getSelectedBaseSoftwareModule().get().getName(), + artifactUploadState.getSelectedBaseSoftwareModule().get().getVersion())); + } + }); + UI.getCurrent().addWindow(confirmDialog.getWindow()); + confirmDialog.getWindow().bringToFront(); + + } + + private void setTableColumnDetails(final Table table) { + + table.setColumnHeader(PROVIDED_FILE_NAME, i18n.get("upload.file.name")); + table.setColumnHeader(SIZE, i18n.get("upload.size")); + if (fullWindowMode) { + table.setColumnHeader(SHA1HASH, i18n.get("upload.sha1")); + table.setColumnHeader(MD5HASH, i18n.get("upload.md5")); + } + table.setColumnHeader(CREATE_MODIFIED_DATE_UPLOAD, i18n.get("upload.last.modified.date")); + if (!readOnly) { + table.setColumnHeader(ACTION, i18n.get("upload.action")); + } + + table.setColumnExpandRatio(PROVIDED_FILE_NAME, 3.5f); + table.setColumnExpandRatio(SIZE, 2f); + if (fullWindowMode) { + table.setColumnExpandRatio(SHA1HASH, 2.8f); + table.setColumnExpandRatio(MD5HASH, 2.4f); + } + table.setColumnExpandRatio(CREATE_MODIFIED_DATE_UPLOAD, 3f); + if (!readOnly) { + table.setColumnExpandRatio(ACTION, 2.5f); + } + + table.setVisibleColumns(getVisbleColumns().toArray()); + } + + private List getVisbleColumns() { + final List visibileColumn = new ArrayList<>(); + visibileColumn.add(PROVIDED_FILE_NAME); + visibileColumn.add(SIZE); + if (fullWindowMode) { + visibileColumn.add(SHA1HASH); + visibileColumn.add(MD5HASH); + } + visibileColumn.add(CREATE_MODIFIED_DATE_UPLOAD); + if (!readOnly) { + visibileColumn.add(ACTION); + } + return visibileColumn; + } + + private Table createArtifactDetailsTable() { + final Table detailsTable = new Table(); + detailsTable.addStyleName("sp-table"); + + detailsTable.setImmediate(true); + detailsTable.setSizeFull(); + + detailsTable.setId(SPUIComponetIdProvider.UPLOAD_ARTIFACT_DETAILS_TABLE); + detailsTable.addStyleName(ValoTheme.TABLE_NO_VERTICAL_LINES); + detailsTable.addStyleName(ValoTheme.TABLE_SMALL); + return detailsTable; + } + + /** + * will be used by button click listener of action history expand icon. + */ + private void maxArtifactDetails() { + final Boolean flag = (Boolean) maxMinButton.getData(); + if (flag == null || Boolean.FALSE.equals(flag)) { + // Clicked on max Button + maximizedArtifactDetailsView(); + } else { + // Clicked on min Button + minimizeArtifactDetailsView(); + } + } + + private void minimizeArtifactDetailsView() { + fullWindowMode = Boolean.FALSE; + showMaxIcon(); + setTableColumnDetails(artifactDetailsTable); + createArtifactDetailsMinView(); + + } + + private void maximizedArtifactDetailsView() { + fullWindowMode = Boolean.TRUE; + showMinIcon(); + setTableColumnDetails(artifactDetailsTable); + createArtifactDetailsMaxView(); + + } + + /** + * Create Max artifact details Table. + */ + public void createMaxArtifactDetailsTable() { + maxArtifactDetailsTable = createArtifactDetailsTable(); + maxArtifactDetailsTable.setId(SPUIComponetIdProvider.UPLOAD_ARTIFACT_DETAILS_TABLE_MAX); + maxArtifactDetailsTable.setContainerDataSource(artifactDetailsTable.getContainerDataSource()); + addGeneratedColumn(maxArtifactDetailsTable); + if (!readOnly) { + addGeneratedColumnButton(maxArtifactDetailsTable); + } + setTableColumnDetails(maxArtifactDetailsTable); + } + + private void createArtifactDetailsMaxView() { + + artifactDetailsTable.setValue(null); + artifactDetailsTable.setSelectable(false); + artifactDetailsTable.setMultiSelect(false); + artifactDetailsTable.setDragMode(TableDragMode.NONE); + artifactDetailsTable.setColumnCollapsingAllowed(true); + artifactUploadState.setArtifactDetailsMaximized(Boolean.TRUE); + eventBus.publish(this, ArtifactDetailsEvent.MAXIMIZED); + } + + private void createArtifactDetailsMinView() { + artifactUploadState.setArtifactDetailsMaximized(Boolean.FALSE); + artifactDetailsTable.setColumnCollapsingAllowed(false); + eventBus.publish(this, ArtifactDetailsEvent.MINIMIZED); + } + + /** + * Populate artifact details. + * + * @param baseSwModuleId + * software module id + * @param swModuleName + * software module name + */ + public void populateArtifactDetails(final Long baseSwModuleId, final String swModuleName) { + if (!readOnly) { + if (Strings.isNullOrEmpty(swModuleName)) { + setTitleOfLayoutHeader(); + } else { + titleOfArtifactDetails.setValue(HawkbitCommonUtil.getArtifactoryDetailsLabelId(swModuleName)); + titleOfArtifactDetails.setContentMode(ContentMode.HTML); + } + } + final Map queryConfiguration = new HashMap(); + if (baseSwModuleId != null) { + queryConfiguration.put(SPUIDefinitions.BY_BASE_SOFTWARE_MODULE, baseSwModuleId); + } + final LazyQueryContainer artifactContainer = getArtifactLazyQueryContainer(queryConfiguration); + artifactDetailsTable.setContainerDataSource(artifactContainer); + if (fullWindowMode && maxArtifactDetailsTable != null) { + maxArtifactDetailsTable.setContainerDataSource(artifactContainer); + } + setTableColumnDetails(artifactDetailsTable); + + } + + /** + * Set title of artifact details header layout. + */ + public void setTitleOfLayoutHeader() { + titleOfArtifactDetails.setValue(HawkbitCommonUtil.getArtifactoryDetailsLabelId("")); + titleOfArtifactDetails.setContentMode(ContentMode.HTML); + } + + /** + * Close artifact details layout. + */ + public void closeArtifactDetails() { + removeAllComponents(); + setVisible(false); + } + + @EventBusListenerMethod(scope = EventScope.SESSION) + void onEvent(final SoftwareModuleEvent softwareModuleEvent) { + if (softwareModuleEvent.getSoftwareModuleEventType() == SoftwareModuleEventType.SELECTED_SOFTWARE_MODULE) { + ui.access(() -> { + if (softwareModuleEvent.getSoftwareModule() != null) { + populateArtifactDetails(softwareModuleEvent.getSoftwareModule().getId(), + HawkbitCommonUtil.getFormattedNameVersion(softwareModuleEvent.getSoftwareModule().getName(), + softwareModuleEvent.getSoftwareModule().getVersion())); + } else { + populateArtifactDetails(null, null); + } + }); + } + if (softwareModuleEvent.getSoftwareModuleEventType() == SoftwareModuleEventType.ARTIFACTS_CHANGED) { + ui.access(() -> { + if (softwareModuleEvent.getSoftwareModule() != null) { + populateArtifactDetails(softwareModuleEvent.getSoftwareModule().getId(), + HawkbitCommonUtil.getFormattedNameVersion(softwareModuleEvent.getSoftwareModule().getName(), + softwareModuleEvent.getSoftwareModule().getVersion())); + } else { + populateArtifactDetails(null, null); + } + }); + } + } + + @PreDestroy + void destroy() { + eventBus.unsubscribe(this); // It's good manners to do this, even though + // we should be + // automatically unsubscribed when the UI is + // garbage collected + } + + public Table getArtifactDetailsTable() { + return artifactDetailsTable; + } + + public Table getMaxArtifactDetailsTable() { + return maxArtifactDetailsTable; + } + + public void setFullWindowMode(final boolean fullWindowMode) { + this.fullWindowMode = fullWindowMode; + } + + private void showMinIcon() { + maxMinButton.togleIcon(FontAwesome.COMPRESS); + maxMinButton.setData(Boolean.TRUE); + } + + private void showMaxIcon() { + maxMinButton.togleIcon(FontAwesome.EXPAND); + maxMinButton.setData(Boolean.FALSE); + } + + private boolean isMaximized() { + return artifactUploadState.isArtifactDetailsMaximized(); + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/ArtifactDetailsEvent.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/ArtifactDetailsEvent.java new file mode 100755 index 000000000..10312647d --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/ArtifactDetailsEvent.java @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.event; + +/** + * + * + */ +public enum ArtifactDetailsEvent { + MAXIMIZED, MINIMIZED, + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/SMFilterEvent.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/SMFilterEvent.java new file mode 100755 index 000000000..95fefcee4 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/SMFilterEvent.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.event; + +/** + * Software module filter events. + * + * + * + */ +public enum SMFilterEvent { + + FILTER_BY_TYPE, + + FILTER_BY_TEXT, + + REMOVER_FILTER_BY_TYPE, + + REMOVER_FILTER_BY_TEXT +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/SoftwareModuleEvent.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/SoftwareModuleEvent.java new file mode 100755 index 000000000..3d0897dcd --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/SoftwareModuleEvent.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.event; + +import org.eclipse.hawkbit.repository.model.SoftwareModule; + +/** + * Event to represent software add, update or delete. + * + * + * + */ +public class SoftwareModuleEvent { + + /** + * Software module events in the Upload UI. + * + * + * + */ + public enum SoftwareModuleEventType { + NEW_SOFTWARE_MODULE, UPDATED_SOFTWARE_MODULE, DELETE_SOFTWARE_MODULE, SELECTED_SOFTWARE_MODULE, MAXIMIZED, MINIMIZED, ARTIFACTS_CHANGED, ASSIGN_SOFTWARE_MODULE + } + + private SoftwareModuleEventType softwareModuleEventType; + + private SoftwareModule softwareModule; + + /** + * Creates software module event. + * + * @param softwareModuleEventType + * reference of {@link SoftwareModuleEventType} + * @param softwareModule + * reference of {@link SoftwareModule} + */ + public SoftwareModuleEvent(final SoftwareModuleEventType softwareModuleEventType, + final SoftwareModule softwareModule) { + super(); + this.softwareModuleEventType = softwareModuleEventType; + this.softwareModule = softwareModule; + } + + public SoftwareModuleEventType getSoftwareModuleEventType() { + return softwareModuleEventType; + } + + public void setSoftwareModuleEventType(final SoftwareModuleEventType softwareModuleEventType) { + this.softwareModuleEventType = softwareModuleEventType; + } + + public SoftwareModule getSoftwareModule() { + return softwareModule; + } + + public void setSoftwareModule(final SoftwareModule softwareModule) { + this.softwareModule = softwareModule; + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/SoftwareModuleTypeEvent.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/SoftwareModuleTypeEvent.java new file mode 100755 index 000000000..88e076775 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/SoftwareModuleTypeEvent.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.event; + +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; + +/** + * Event to represent software module type add, update or delete. + * + * + * + */ +public class SoftwareModuleTypeEvent { + + /** + * Software module type events in the Upload UI. + * + * + * + */ + public enum SoftwareModuleTypeEnum { + ADD_SOFTWARE_MODULE_TYPE, DELETE_SOFTWARE_MODULE_TYPE, UPDATE_SOFTWARE_MODULE_TYPE + } + + private SoftwareModuleType softwareModuleType; + + private final SoftwareModuleTypeEnum softwareModuleTypeEnum; + + /** + * @param softwareModuleTypeEnum + * @param softwareModuleType + */ + public SoftwareModuleTypeEvent(final SoftwareModuleTypeEnum softwareModuleTypeEnum, + final SoftwareModuleType softwareModuleType) { + this.softwareModuleTypeEnum = softwareModuleTypeEnum; + this.softwareModuleType = softwareModuleType; + } + + public SoftwareModuleTypeEnum getSoftwareModuleTypeEnum() { + return softwareModuleTypeEnum; + } + + public SoftwareModuleType getSoftwareModuleType() { + return softwareModuleType; + } + + public void setSoftwareModuleType(final SoftwareModuleType softwareModuleType) { + this.softwareModuleType = softwareModuleType; + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/UploadArtifactUIEvent.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/UploadArtifactUIEvent.java new file mode 100755 index 000000000..3d75b67a7 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/UploadArtifactUIEvent.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.event; + +/** + * Event generated in Upload artifact UI. + * + * + * + */ +public enum UploadArtifactUIEvent { + + SHOW_DROP_HINTS, HIDE_DROP_HINTS, SOFTWARE_DRAG_START, SOFTWARE_TYPE_DRAG_START, UPDATE_UPLOAD_COUNT, ENABLE_PROCESS_BUTTON, HIDE_FILTER_BY_TYPE, SHOW_FILTER_BY_TYPE, DISCARD_DELETE_SOFTWARE, DISCARD_ALL_DELETE_SOFTWARE, DELETED_ALL_SOFWARE, DISABLE_PROCESS_BUTTON, DISCARD_DELETE_SOFTWARE_TYPE, DISCARD_ALL_DELETE_SOFTWARE_TYPE, DELETED_ALL_SOFWARE_TYPE +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/UploadViewAcceptCriteria.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/UploadViewAcceptCriteria.java new file mode 100755 index 000000000..a4ae322fa --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/event/UploadViewAcceptCriteria.java @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.event; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.hawkbit.ui.common.AbstractAcceptCriteria; +import org.eclipse.hawkbit.ui.management.event.DragEvent; +import org.eclipse.hawkbit.ui.utils.SPUIComponetIdProvider; +import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; +import org.eclipse.hawkbit.ui.utils.UINotification; +import org.springframework.beans.factory.annotation.Autowired; +import org.vaadin.spring.events.EventBus; + +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; +import com.vaadin.ui.Component; + +/** + * Upload UI View for Accept criteria. + * + * + * + */ +@SpringComponent +@ViewScope +public class UploadViewAcceptCriteria extends AbstractAcceptCriteria { + + private static final long serialVersionUID = 5158811326115667378L; + + private static final Map> DROP_CONFIGS = createDropConfigurations(); + + private static final Map DROP_HINTS_CONFIGS = createDropHintConfigurations(); + + @Autowired + private transient UINotification uiNotification; + + @Autowired + private transient EventBus.SessionEventBus eventBus; + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.common.AbstractAcceptCriteria#analyseDragComponent + * (com.vaadin.event .dd.DragAndDropEvent, com.vaadin.ui.Component) + */ + @Override + protected void analyseDragComponent(final Component compsource) { + final String sourceID = getComponentId(compsource); + final Object event = DROP_HINTS_CONFIGS.get(sourceID); + eventBus.publish(this, event); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.AbstractAcceptCriteria#hideDropHints + * () + */ + @Override + protected void hideDropHints() { + eventBus.publish(this, DragEvent.HIDE_DROP_HINT); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.AbstractAcceptCriteria#invalidDrop() + */ + @Override + protected void invalidDrop() { + uiNotification.displayValidationError(SPUILabelDefinitions.ACTION_NOT_ALLOWED); + } + + @Override + protected String getComponentId(final Component component) { + String id = component.getId(); + if (id != null && id.startsWith(SPUIComponetIdProvider.UPLOAD_TYPE_BUTTON_PREFIX)) { + id = SPUIComponetIdProvider.UPLOAD_TYPE_BUTTON_PREFIX; + } + return id; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.AbstractAcceptCriteria# + * getDropHintConfigurations() + */ + @Override + protected Map getDropHintConfigurations() { + return DROP_HINTS_CONFIGS; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.AbstractAcceptCriteria# + * publishDragStartEvent(java.lang.Object) + */ + @Override + protected void publishDragStartEvent(final Object event) { + eventBus.publish(this, event); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.AbstractAcceptCriteria# + * getDropConfigurations() + */ + @Override + protected Map> getDropConfigurations() { + return DROP_CONFIGS; + } + + private static Map> createDropConfigurations() { + final Map> config = new HashMap>(); + // Delete drop area droppable components + config.put(SPUIComponetIdProvider.DELETE_BUTTON_WRAPPER_ID, Arrays.asList( + SPUIComponetIdProvider.UPLOAD_SOFTWARE_MODULE_TABLE, SPUIComponetIdProvider.UPLOAD_TYPE_BUTTON_PREFIX)); + + return config; + } + + private static Map createDropHintConfigurations() { + final Map config = new HashMap(); + config.put(SPUIComponetIdProvider.UPLOAD_TYPE_BUTTON_PREFIX, UploadArtifactUIEvent.SOFTWARE_TYPE_DRAG_START); + config.put(SPUIComponetIdProvider.UPLOAD_SOFTWARE_MODULE_TABLE, UploadArtifactUIEvent.SOFTWARE_DRAG_START); + return config; + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/footer/SMDeleteActionsLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/footer/SMDeleteActionsLayout.java new file mode 100755 index 000000000..42961a0fe --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/footer/SMDeleteActionsLayout.java @@ -0,0 +1,329 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.footer; + +import java.util.Set; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.eclipse.hawkbit.repository.SpPermissionChecker; +import org.eclipse.hawkbit.ui.artifacts.event.UploadArtifactUIEvent; +import org.eclipse.hawkbit.ui.artifacts.event.UploadViewAcceptCriteria; +import org.eclipse.hawkbit.ui.artifacts.state.ArtifactUploadState; +import org.eclipse.hawkbit.ui.common.footer.AbstractDeleteActionsLayout; +import org.eclipse.hawkbit.ui.management.event.DragEvent; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPUIComponetIdProvider; +import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; +import org.eclipse.hawkbit.ui.utils.UINotification; +import org.springframework.beans.factory.annotation.Autowired; +import org.vaadin.spring.events.EventBus; +import org.vaadin.spring.events.EventScope; +import org.vaadin.spring.events.annotation.EventBusListenerMethod; + +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.acceptcriteria.AcceptCriterion; +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; +import com.vaadin.ui.Component; +import com.vaadin.ui.Label; +import com.vaadin.ui.Table; +import com.vaadin.ui.UI; + +/** + * Upload view footer layout implementation. + * + * + * + */ +@SpringComponent +@ViewScope +public class SMDeleteActionsLayout extends AbstractDeleteActionsLayout { + + private static final long serialVersionUID = -3273982053389866299L; + + @Autowired + private I18N i18n; + + @Autowired + private SpPermissionChecker permChecker; + + @Autowired + private transient EventBus.SessionEventBus eventBus; + + @Autowired + private transient UINotification notification; + + @Autowired + private ArtifactUploadState artifactUploadState; + + @Autowired + private UploadViewConfirmationWindowLayout uploadViewConfirmationWindowLayout; + + @Autowired + private UploadViewAcceptCriteria uploadViewAcceptCriteria; + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.footer.DeleteActionsLayout#init() + */ + @PostConstruct + protected void init() { + super.init(); + eventBus.subscribe(this); + } + + @PreDestroy + void destroy() { + /* + * It's good manners to do this, even though vaadin-spring will + * automatically unsubscribe when this UI is garbage collected. + */ + eventBus.unsubscribe(this); + } + + @EventBusListenerMethod(scope = EventScope.SESSION) + void onEvent(final UploadArtifactUIEvent event) { + + if (isSoftwareEvent(event) || isSoftwareTypeEvent(event)) { + + UI.getCurrent().access(() -> { + if (!hasUnsavedActions()) { + closeUnsavedActionsWindow(); + final String message = uploadViewConfirmationWindowLayout.getConsolidatedMessage(); + if (message != null && message.length() > 0) { + notification.displaySuccess(message); + } + } + updateSWActionCount(); + }); + } + if (event == UploadArtifactUIEvent.SOFTWARE_DRAG_START + || event == UploadArtifactUIEvent.SOFTWARE_TYPE_DRAG_START) { + showDropHints(); + } + } + + private boolean isSoftwareEvent(final UploadArtifactUIEvent event) { + return event == UploadArtifactUIEvent.DISCARD_ALL_DELETE_SOFTWARE + || event == UploadArtifactUIEvent.DELETED_ALL_SOFWARE + || event == UploadArtifactUIEvent.DISCARD_DELETE_SOFTWARE; + } + + private boolean isSoftwareTypeEvent(final UploadArtifactUIEvent event) { + return event == UploadArtifactUIEvent.DISCARD_ALL_DELETE_SOFTWARE_TYPE + || event == UploadArtifactUIEvent.DELETED_ALL_SOFWARE_TYPE + || event == UploadArtifactUIEvent.DISCARD_DELETE_SOFTWARE_TYPE; + } + + @EventBusListenerMethod(scope = EventScope.SESSION) + void onEvent(final DragEvent event) { + if (event == DragEvent.HIDE_DROP_HINT) { + UI.getCurrent().access(() -> hideDropHints()); + } + + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.footer.DeleteActionsLayout# + * hasDeletePermission() + */ + @Override + protected boolean hasDeletePermission() { + return permChecker.hasDeleteDistributionPermission(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.footer.DeleteActionsLayout# + * hasUpdatePermission() + */ + @Override + protected boolean hasUpdatePermission() { + return permChecker.hasUpdateDistributionPermission(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.footer.DeleteActionsLayout# + * getDeleteAreaLabel() + */ + @Override + protected String getDeleteAreaLabel() { + return i18n.get("label.software.module.drop.area"); + } + + @Override + protected String getDeleteAreaId() { + return SPUIComponetIdProvider.DELETE_BUTTON_WRAPPER_ID; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.footer.DeleteActionsLayout# + * getDeleteLayoutAcceptCriteria() + */ + @Override + protected AcceptCriterion getDeleteLayoutAcceptCriteria() { + return uploadViewAcceptCriteria; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.footer.DeleteActionsLayout# + * processDroppedComponent(com.vaadin .event.dd.DragAndDropEvent) + */ + @Override + protected void processDroppedComponent(final DragAndDropEvent event) { + final Component sourceComponent = event.getTransferable().getSourceComponent(); + if (sourceComponent instanceof Table) { + final Table sourceTable = (Table) event.getTransferable().getSourceComponent(); + addToDeleteList(sourceTable); + updateSWActionCount(); + } + if (sourceComponent.getId().startsWith(SPUIComponetIdProvider.UPLOAD_TYPE_BUTTON_PREFIX)) { + + final String swModuleTypeName = sourceComponent.getId() + .replace(SPUIComponetIdProvider.UPLOAD_TYPE_BUTTON_PREFIX, ""); + if (artifactUploadState.getSoftwareModuleFilters().getSoftwareModuleType().isPresent() + && artifactUploadState.getSoftwareModuleFilters().getSoftwareModuleType().get().getName() + .equalsIgnoreCase(swModuleTypeName)) { + notification.displayValidationError( + i18n.get("message.swmodule.type.check.delete", new Object[] { swModuleTypeName })); + } else { + deleteSWModuleType(swModuleTypeName); + updateSWActionCount(); + } + + } + hideDropHints(); + } + + private void deleteSWModuleType(final String swModuleTypeName) { + artifactUploadState.getSelectedDeleteSWModuleTypes().add(swModuleTypeName); + } + + private void addToDeleteList(final Table sourceTable) { + final Set swModuleIds = (Set) sourceTable.getValue(); + swModuleIds.forEach(id -> { + final String swModuleName = (String) sourceTable.getContainerDataSource().getItem(id) + .getItemProperty(SPUILabelDefinitions.NAME_VERSION).getValue(); + artifactUploadState.getDeleteSofwareModules().put(id, swModuleName); + }); + } + + /** + * Update the software module delete count. + */ + private void updateSWActionCount() { + final int count = artifactUploadState.getDeleteSofwareModules().size() + + artifactUploadState.getSelectedDeleteSWModuleTypes().size(); + updateActionsCount(count); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.footer.DeleteActionsLayout# + * getNoActionsButtonLabel() + */ + @Override + protected String getNoActionsButtonLabel() { + return i18n.get("button.no.actions"); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.footer.DeleteActionsLayout# + * getActionsButtonLabel() + */ + @Override + protected String getActionsButtonLabel() { + return i18n.get("button.actions"); + } + + @Override + protected void reloadActionCount() { + updateSWActionCount(); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.footer.AbstractDeleteActionsLayout# + * getUnsavedActionsWindowCaption () + */ + @Override + protected String getUnsavedActionsWindowCaption() { + return i18n.get("caption.save.window"); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.footer.AbstractDeleteActionsLayout# + * unsavedActionsWindowClosed() + */ + @Override + protected void unsavedActionsWindowClosed() { + final String message = uploadViewConfirmationWindowLayout.getConsolidatedMessage(); + if (message != null && message.length() > 0) { + notification.displaySuccess(message); + } + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.footer.AbstractDeleteActionsLayout# + * getUnsavedActionsWindowContent () + */ + @Override + protected Component getUnsavedActionsWindowContent() { + uploadViewConfirmationWindowLayout.init(); + return uploadViewConfirmationWindowLayout; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.footer.AbstractDeleteActionsLayout# + * hasUnsavedActions() + */ + @Override + protected boolean hasUnsavedActions() { + return !artifactUploadState.getDeleteSofwareModules().isEmpty() + || !artifactUploadState.getSelectedDeleteSWModuleTypes().isEmpty(); + } + + @Override + protected boolean hasCountMessage() { + return false; + } + + @Override + protected Label getCountMessageLabel() { + return null; + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/footer/UploadViewConfirmationWindowLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/footer/UploadViewConfirmationWindowLayout.java new file mode 100755 index 000000000..537a76a70 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/footer/UploadViewConfirmationWindowLayout.java @@ -0,0 +1,319 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.footer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.PostConstruct; + +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.ui.artifacts.event.UploadArtifactUIEvent; +import org.eclipse.hawkbit.ui.artifacts.state.ArtifactUploadState; +import org.eclipse.hawkbit.ui.artifacts.state.CustomFile; +import org.eclipse.hawkbit.ui.common.confirmwindow.layout.AbstractConfirmationWindowLayout; +import org.eclipse.hawkbit.ui.common.confirmwindow.layout.ConfirmationTab; +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleSmallNoBorder; +import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPUIComponetIdProvider; +import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; +import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; +import org.springframework.beans.factory.annotation.Autowired; +import org.vaadin.spring.events.EventBus; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.FontAwesome; +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; +import com.vaadin.ui.Button; +import com.vaadin.ui.Table.Align; +import com.vaadin.ui.themes.ValoTheme; + +/** + * Abstract layout of confirm actions window. + * + * + * + */ +@SpringComponent +@ViewScope +public class UploadViewConfirmationWindowLayout extends AbstractConfirmationWindowLayout { + + private static final long serialVersionUID = 1804036019105286988L; + + private static final String SW_MODULE_NAME_MSG = "SW MOdule Name"; + + private static final String SW_DISCARD_CHGS = "DiscardChanges"; + + private static final String SW_MODULE_TYPE_NAME = "SoftwareModuleTypeName"; + + private static final String DISCARD = "Discard"; + + @Autowired + private I18N i18n; + + @Autowired + private transient SoftwareManagement softwareManagement; + + @Autowired + private transient EventBus.SessionEventBus eventBus; + + @Autowired + private ArtifactUploadState artifactUploadState; + + /** + * Initialze the component. + */ + @PostConstruct + void init() { + super.inittialize(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.confirmwindow.layout. + * AbstractConfirmationWindowLayout# getConfimrationTabs() + */ + @Override + protected Map getConfimrationTabs() { + final Map tabs = new HashMap(); + if (!artifactUploadState.getDeleteSofwareModules().isEmpty()) { + tabs.put(i18n.get("caption.delete.swmodule.accordion.tab"), createSMDeleteConfirmationTab()); + } + if (!artifactUploadState.getSelectedDeleteSWModuleTypes().isEmpty()) { + tabs.put(i18n.get("caption.delete.sw.module.type.accordion.tab"), createSMtypeDeleteConfirmationTab()); + } + return tabs; + } + + private ConfirmationTab createSMDeleteConfirmationTab() { + final ConfirmationTab tab = new ConfirmationTab(); + + tab.getConfirmAll().setId(SPUIComponetIdProvider.SW_DELETE_ALL); + tab.getConfirmAll().setIcon(FontAwesome.TRASH_O); + tab.getConfirmAll().setCaption(i18n.get("button.delete.all")); + tab.getConfirmAll().addClickListener(event -> deleteSMAll(tab)); + + tab.getDiscardAll().setCaption(i18n.get("button.discard.all")); + tab.getDiscardAll().addClickListener(event -> discardSMAll(tab)); + + // Add items container to the table. + tab.getTable().setContainerDataSource(getSWModuleTableContainer()); + + // Add the discard action column + tab.getTable().addGeneratedColumn(SW_DISCARD_CHGS, (source, itemId, columnId) -> { + final Button deleteswIcon = SPUIComponentProvider.getButton("", "", SPUILabelDefinitions.DISCARD, + ValoTheme.BUTTON_TINY + " " + "redicon", true, FontAwesome.REPLY, + SPUIButtonStyleSmallNoBorder.class); + deleteswIcon.setData(itemId); + deleteswIcon.setImmediate(true); + deleteswIcon.addClickListener(event -> discardSoftwareDelete(event, itemId, tab)); + return deleteswIcon; + }); + + // set the visible columns + final List visibleColumnIds = new ArrayList<>(); + final List visibleColumnLabels = new ArrayList<>(); + if (visibleColumnIds.isEmpty() && visibleColumnLabels.isEmpty()) { + visibleColumnIds.add(SW_MODULE_NAME_MSG); + visibleColumnIds.add(SW_DISCARD_CHGS); + visibleColumnLabels.add(i18n.get("upload.swModuleTable.header")); + visibleColumnLabels.add(i18n.get("header.second.deletetarget.table")); + } + tab.getTable().setVisibleColumns(visibleColumnIds.toArray()); + tab.getTable().setColumnHeaders(visibleColumnLabels.toArray(new String[0])); + + tab.getTable().setColumnExpandRatio(SW_MODULE_NAME_MSG, SPUIDefinitions.TARGET_DISTRIBUTION_COLUMN_WIDTH); + tab.getTable().setColumnExpandRatio(SW_DISCARD_CHGS, SPUIDefinitions.DISCARD_COLUMN_WIDTH); + tab.getTable().setColumnAlignment(SW_DISCARD_CHGS, Align.CENTER); + return tab; + } + + /** + * Get SWModule table container. + * + * @return IndexedContainer + */ + @SuppressWarnings("unchecked") + private IndexedContainer getSWModuleTableContainer() { + final IndexedContainer swcontactContainer = new IndexedContainer(); + swcontactContainer.addContainerProperty("SWModuleId", String.class, ""); + swcontactContainer.addContainerProperty(SW_MODULE_NAME_MSG, String.class, ""); + Item item = null; + for (final Long swModuleID : artifactUploadState.getDeleteSofwareModules().keySet()) { + item = swcontactContainer.addItem(swModuleID); + item.getItemProperty("SWModuleId").setValue(swModuleID.toString()); + item.getItemProperty(SW_MODULE_NAME_MSG) + .setValue(artifactUploadState.getDeleteSofwareModules().get(swModuleID)); + } + return swcontactContainer; + } + + private void discardSoftwareDelete(final Button.ClickEvent event, final Object itemId, final ConfirmationTab tab) { + + final Long swmoduleId = (Long) ((Button) event.getComponent()).getData(); + if (null != artifactUploadState.getDeleteSofwareModules() + && !artifactUploadState.getDeleteSofwareModules().isEmpty() + && artifactUploadState.getDeleteSofwareModules().containsKey(swmoduleId)) { + artifactUploadState.getDeleteSofwareModules().remove(swmoduleId); + } + tab.getTable().getContainerDataSource().removeItem(itemId); + final int deleteCount = tab.getTable().size(); + if (0 == deleteCount) { + removeCurrentTab(tab); + eventBus.publish(this, UploadArtifactUIEvent.DISCARD_ALL_DELETE_SOFTWARE); + } else { + eventBus.publish(this, UploadArtifactUIEvent.DISCARD_DELETE_SOFTWARE); + } + } + + private void deleteSMAll(final ConfirmationTab tab) { + final Set swmoduleIds = artifactUploadState.getDeleteSofwareModules().keySet(); + softwareManagement.deleteSoftwareModules(swmoduleIds); + addToConsolitatedMsg(FontAwesome.TRASH_O.getHtml() + SPUILabelDefinitions.HTML_SPACE + + i18n.get("message.swModule.deleted", artifactUploadState.getDeleteSofwareModules().size())); + /* + * Check if any information / files pending to upload for the deleted + * software modules. If so, then delete the files from the upload list. + */ + final List tobeRemoved = new ArrayList<>(); + for (final Long id : swmoduleIds) { + final String deleteSoftwareNameVersion = artifactUploadState.getDeleteSofwareModules().get(id); + + for (final CustomFile customFile : artifactUploadState.getFileSelected()) { + final String swNameVersion = HawkbitCommonUtil.getFormattedNameVersion( + customFile.getBaseSoftwareModuleName(), customFile.getBaseSoftwareModuleVersion()); + if (HawkbitCommonUtil.bothSame(deleteSoftwareNameVersion, swNameVersion)) { + tobeRemoved.add(customFile); + } + } + } + if (!tobeRemoved.isEmpty()) { + artifactUploadState.getFileSelected().removeAll(tobeRemoved); + } + artifactUploadState.getDeleteSofwareModules().clear(); + removeCurrentTab(tab); + setActionMessage(i18n.get("message.software.delete.success")); + eventBus.publish(this, UploadArtifactUIEvent.DELETED_ALL_SOFWARE); + } + + private void discardSMAll(final ConfirmationTab tab) { + removeCurrentTab(tab); + artifactUploadState.getDeleteSofwareModules().clear(); + setActionMessage(i18n.get("message.software.discard.success")); + eventBus.publish(this, UploadArtifactUIEvent.DISCARD_ALL_DELETE_SOFTWARE); + } + + private ConfirmationTab createSMtypeDeleteConfirmationTab() { + final ConfirmationTab tab = new ConfirmationTab(); + + tab.getConfirmAll().setId(SPUIComponetIdProvider.SAVE_DELETE_SW_MODULE_TYPE); + tab.getConfirmAll().setIcon(FontAwesome.TRASH_O); + tab.getConfirmAll().setCaption(i18n.get("button.delete.all")); + tab.getConfirmAll().addClickListener(event -> deleteSMtypeAll(tab)); + + tab.getDiscardAll().setCaption(i18n.get("button.discard.all")); + tab.getDiscardAll().addClickListener(event -> discardSMtypeAll(tab)); + + // Add items container to the table. + tab.getTable().setContainerDataSource(getSWModuleTypeTableContainer()); + + // Add the discard action column + tab.getTable().addGeneratedColumn(DISCARD, (source, itemId, columnId) -> { + final StringBuilder style = new StringBuilder(ValoTheme.BUTTON_TINY); + style.append(' '); + style.append("redicon"); + final Button deleteIcon = SPUIComponentProvider.getButton("", "", SPUILabelDefinitions.DISCARD, + style.toString(), true, FontAwesome.REPLY, SPUIButtonStyleSmallNoBorder.class); + deleteIcon.setData(itemId); + deleteIcon.setImmediate(true); + deleteIcon.addClickListener(event -> discardSoftwareTypeDelete( + (String) ((Button) event.getComponent()).getData(), itemId, tab)); + return deleteIcon; + }); + + // set the visible columns + final List visibleColumnIds = new ArrayList<>(); + final List visibleColumnLabels = new ArrayList<>(); + if (visibleColumnIds.isEmpty() && visibleColumnLabels.isEmpty()) { + visibleColumnIds.add(SW_MODULE_TYPE_NAME); + visibleColumnIds.add(DISCARD); + visibleColumnLabels.add(i18n.get("header.first.delete.swmodule.type.table")); + visibleColumnLabels.add(i18n.get("header.second.delete.swmodule.type.table")); + + } + tab.getTable().setVisibleColumns(visibleColumnIds.toArray()); + tab.getTable().setColumnHeaders(visibleColumnLabels.toArray(new String[0])); + + tab.getTable().setColumnExpandRatio(SW_MODULE_TYPE_NAME, 2); + tab.getTable().setColumnExpandRatio(SW_DISCARD_CHGS, SPUIDefinitions.DISCARD_COLUMN_WIDTH); + tab.getTable().setColumnAlignment(SW_DISCARD_CHGS, Align.CENTER); + return tab; + } + + /** + * @return + */ + private Container getSWModuleTypeTableContainer() { + final IndexedContainer contactContainer = new IndexedContainer(); + contactContainer.addContainerProperty(SW_MODULE_TYPE_NAME, String.class, ""); + for (final String swModuleTypeName : artifactUploadState.getSelectedDeleteSWModuleTypes()) { + final Item saveTblitem = contactContainer.addItem(swModuleTypeName); + saveTblitem.getItemProperty(SW_MODULE_TYPE_NAME).setValue(swModuleTypeName); + } + return contactContainer; + } + + private void discardSoftwareTypeDelete(final String discardSWModuleType, final Object itemId, + final ConfirmationTab tab) { + if (null != artifactUploadState.getSelectedDeleteSWModuleTypes() + && !artifactUploadState.getSelectedDeleteSWModuleTypes().isEmpty() + && artifactUploadState.getSelectedDeleteSWModuleTypes().contains(discardSWModuleType)) { + artifactUploadState.getSelectedDeleteSWModuleTypes().remove(discardSWModuleType); + } + tab.getTable().getContainerDataSource().removeItem(itemId); + final int deleteCount = tab.getTable().size(); + if (0 == deleteCount) { + removeCurrentTab(tab); + eventBus.publish(this, UploadArtifactUIEvent.DISCARD_ALL_DELETE_SOFTWARE_TYPE); + } else { + eventBus.publish(this, UploadArtifactUIEvent.DISCARD_DELETE_SOFTWARE_TYPE); + } + } + + private void deleteSMtypeAll(final ConfirmationTab tab) { + final int deleteSWModuleTypeCount = artifactUploadState.getSelectedDeleteSWModuleTypes().size(); + for (final String swModuleTypeName : artifactUploadState.getSelectedDeleteSWModuleTypes()) { + + softwareManagement + .deleteSoftwareModuleType(softwareManagement.findSoftwareModuleTypeByName(swModuleTypeName)); + } + addToConsolitatedMsg(FontAwesome.TASKS.getHtml() + SPUILabelDefinitions.HTML_SPACE + + i18n.get("message.sw.module.type.delete", new Object[] { deleteSWModuleTypeCount })); + artifactUploadState.getSelectedDeleteSWModuleTypes().clear(); + removeCurrentTab(tab); + setActionMessage(i18n.get("message.software.type.delete.success")); + eventBus.publish(this, UploadArtifactUIEvent.DELETED_ALL_SOFWARE_TYPE); + } + + private void discardSMtypeAll(final ConfirmationTab tab) { + removeCurrentTab(tab); + artifactUploadState.getSelectedDeleteSWModuleTypes().clear(); + setActionMessage(i18n.get("message.software.type.discard.success")); + eventBus.publish(this, UploadArtifactUIEvent.DISCARD_ALL_DELETE_SOFTWARE_TYPE); + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/BaseSwModuleBeanQuery.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/BaseSwModuleBeanQuery.java new file mode 100755 index 000000000..04339c1a6 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/BaseSwModuleBeanQuery.java @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.smtable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.eclipse.hawkbit.repository.OffsetBasedPageRequest; +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; +import org.eclipse.hawkbit.ui.utils.SPDateTimeUtil; +import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; +import org.eclipse.hawkbit.ui.utils.SpringContextHelper; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.vaadin.addons.lazyquerycontainer.AbstractBeanQuery; +import org.vaadin.addons.lazyquerycontainer.QueryDefinition; + +import com.google.common.base.Strings; + +/** + * Simple implementation of generics bean query which dynamically loads a batch + * of beans. + * + * + * + * + * + */ +public class BaseSwModuleBeanQuery extends AbstractBeanQuery { + private static final long serialVersionUID = 4362142538539335466L; + private transient SoftwareManagement softwareManagementService; + private SoftwareModuleType type; + private String searchText = null; + private final Sort sort = new Sort(Direction.ASC, "name", "version"); + + /** + * Parametric Constructor. + * + * @param definition + * as Def + * @param queryConfig + * as Config + * @param sortIds + * as sort + * @param sortStates + * as Sort status + */ + public BaseSwModuleBeanQuery(final QueryDefinition definition, final Map queryConfig, + final Object[] sortIds, final boolean[] sortStates) { + super(definition, queryConfig, sortIds, sortStates); + if (HawkbitCommonUtil.mapCheckStrKey(queryConfig)) { + type = (SoftwareModuleType) queryConfig.get(SPUIDefinitions.BY_SOFTWARE_MODULE_TYPE); + searchText = (String) queryConfig.get(SPUIDefinitions.FILTER_BY_TEXT); + if (!Strings.isNullOrEmpty(searchText)) { + searchText = String.format("%%%s%%", searchText); + } + } + } + + @Override + protected ProxyBaseSoftwareModuleItem constructBean() { + return new ProxyBaseSoftwareModuleItem(); + } + + @Override + protected List loadBeans(final int startIndex, final int count) { + final Slice swModuleBeans; + final List proxyBeans = new ArrayList(); + + if (type == null && Strings.isNullOrEmpty(searchText)) { + swModuleBeans = getSoftwareManagementService() + .findSoftwareModulesAll(new OffsetBasedPageRequest(startIndex, count, sort)); + + } else { + swModuleBeans = getSoftwareManagementService() + .findSoftwareModuleByFilters(new OffsetBasedPageRequest(startIndex, count, sort), searchText, type); + } + + for (final SoftwareModule swModule : swModuleBeans) { + proxyBeans.add(getProxyBean(swModule)); + } + + return proxyBeans; + } + + private ProxyBaseSoftwareModuleItem getProxyBean(final SoftwareModule bean) { + final ProxyBaseSoftwareModuleItem proxy = new ProxyBaseSoftwareModuleItem(); + proxy.setSwId(bean.getId()); + final String swNameVersion = HawkbitCommonUtil.concatStrings(":", bean.getName(), bean.getVersion()); + proxy.setNameAndVersion(swNameVersion); + proxy.setCreatedDate(SPDateTimeUtil.getFormattedDate(bean.getCreatedAt())); + proxy.setLastModifiedDate(SPDateTimeUtil.getFormattedDate(bean.getLastModifiedAt())); + proxy.setName(bean.getName()); + proxy.setVersion(bean.getVersion()); + proxy.setVendor(bean.getVendor()); + proxy.setDescription(bean.getDescription()); + proxy.setCreatedByUser(HawkbitCommonUtil.getIMUser(bean.getCreatedBy())); + proxy.setModifiedByUser(HawkbitCommonUtil.getIMUser(bean.getLastModifiedBy())); + return proxy; + } + + @Override + public int size() { + long size; + if (type == null && Strings.isNullOrEmpty(searchText)) { + size = getSoftwareManagementService().countSoftwareModulesAll(); + } else { + size = getSoftwareManagementService().countSoftwareModuleByFilters(searchText, type); + } + + if (size > Integer.MAX_VALUE) { + size = Integer.MAX_VALUE; + } + return (int) size; + + } + + @Override + protected void saveBeans(final List addedBeans, + final List modifiedBeans, + final List removedBeans) { + // save of the entity not required from this method + } + + private SoftwareManagement getSoftwareManagementService() { + if (softwareManagementService == null) { + softwareManagementService = SpringContextHelper.getBean(SoftwareManagement.class); + } + return softwareManagementService; + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/ProxyBaseSoftwareModuleItem.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/ProxyBaseSoftwareModuleItem.java new file mode 100755 index 000000000..6e417435c --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/ProxyBaseSoftwareModuleItem.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.smtable; + +import java.security.SecureRandom; + +import org.eclipse.hawkbit.repository.model.SoftwareModule; + +/** + * + * Proxy for software module to display details in Software modules table. + * + * + * + */ +public class ProxyBaseSoftwareModuleItem extends SoftwareModule { + + private static final long serialVersionUID = -1555306616599140635L; + + private static final SecureRandom RANDOM_OBJ = new SecureRandom(); + + private String nameAndVersion; + + private Long swId; + + private boolean assigned; + + private String createdDate; + + private String lastModifiedDate; + + private String createdByUser; + + private String modifiedByUser; + + /** + * Default constructor. + */ + public ProxyBaseSoftwareModuleItem() { + super(); + swId = RANDOM_OBJ.nextLong(); + } + + public String getCreatedByUser() { + return createdByUser; + } + + public Long getSwId() { + return swId; + } + + public void setCreatedByUser(final String createdByUser) { + this.createdByUser = createdByUser; + } + + public void setSwId(final Long swId) { + this.swId = swId; + } + + public String getModifiedByUser() { + return modifiedByUser; + } + + public void setModifiedByUser(final String modifiedByUser) { + this.modifiedByUser = modifiedByUser; + } + + public String getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(final String createdDate) { + this.createdDate = createdDate; + } + + public String getLastModifiedDate() { + return lastModifiedDate; + } + + public void setLastModifiedDate(final String lastModifiedDate) { + this.lastModifiedDate = lastModifiedDate; + } + + public String getNameAndVersion() { + return nameAndVersion; + } + + public void setNameAndVersion(final String nameAndVersion) { + this.nameAndVersion = nameAndVersion; + } + + public boolean isAssigned() { + return assigned; + } + + public void setAssigned(final boolean assigned) { + this.assigned = assigned; + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleAddUpdateWindow.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleAddUpdateWindow.java new file mode 100755 index 000000000..3cc5c8dc1 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleAddUpdateWindow.java @@ -0,0 +1,371 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.smtable; + +import java.io.Serializable; + +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleEvent; +import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleEvent.SoftwareModuleEventType; +import org.eclipse.hawkbit.ui.common.SoftwareModuleTypeBeanQuery; +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleSmallNoBorder; +import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPUIComponetIdProvider; +import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; +import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; +import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; +import org.eclipse.hawkbit.ui.utils.UINotification; +import org.springframework.beans.factory.annotation.Autowired; +import org.vaadin.addons.lazyquerycontainer.BeanQueryFactory; +import org.vaadin.spring.events.EventBus; + +import com.vaadin.event.FieldEvents.TextChangeEvent; +import com.vaadin.event.FieldEvents.TextChangeListener; +import com.vaadin.server.FontAwesome; +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.Button; +import com.vaadin.ui.ComboBox; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.TextArea; +import com.vaadin.ui.TextField; +import com.vaadin.ui.UI; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; +import com.vaadin.ui.themes.ValoTheme; + +/** + * Generates window for Software module add or update. + * + * + */ +@SpringComponent +@ViewScope +public class SoftwareModuleAddUpdateWindow implements Serializable { + + private static final long serialVersionUID = -5217675246477211483L; + + @Autowired + private I18N i18n; + + @Autowired + private transient UINotification uiNotifcation; + + @Autowired + private transient EventBus.SessionEventBus eventBus; + + @Autowired + private transient SoftwareManagement softwareManagement; + + private Label madatoryLabel; + + private TextField nameTextField; + + private TextField versionTextField; + + private TextField vendorTextField; + + private Button saveSoftware; + + private Button closeWindow; + + private ComboBox typeComboBox; + + private TextArea descTextArea; + + private Window window; + + private String oldDescriptionValue; + + private String oldVendorValue; + + private Boolean editSwModule = Boolean.FALSE; + + private Long baseSwModuleId; + + /** + * Create window for new software module. + * + * @return reference of {@link com.vaadin.ui.Window} to add new software + * module. + */ + public Window createAddSoftwareModuleWindow() { + editSwModule = Boolean.FALSE; + createRequiredComponents(); + createWindow(); + return window; + } + + /** + * Create window for update software module. + * + * @param baseSwModuleId + * is id of the software module to edit. + * @return reference of {@link com.vaadin.ui.Window} to update software + * module. + */ + public Window createUpdateSoftwareModuleWindow(final Long baseSwModuleId) { + editSwModule = Boolean.TRUE; + this.baseSwModuleId = baseSwModuleId; + createRequiredComponents(); + createWindow(); + /* populate selected target values to edit. */ + populateValuesOfSwModule(); + nameTextField.setEnabled(false); + versionTextField.setEnabled(false); + typeComboBox.setEnabled(false); + return window; + } + + private void createRequiredComponents() { + /* name textfield */ + nameTextField = SPUIComponentProvider.getTextField("", ValoTheme.TEXTFIELD_TINY, true, null, + i18n.get("textfield.name"), true, SPUILabelDefinitions.TEXT_FIELD_MAX_LENGTH); + nameTextField.setId(SPUIComponetIdProvider.SOFT_MODULE_NAME); + + /* version text field */ + versionTextField = SPUIComponentProvider.getTextField("", ValoTheme.TEXTFIELD_TINY, true, null, + i18n.get("textfield.version"), true, SPUILabelDefinitions.TEXT_FIELD_MAX_LENGTH); + versionTextField.setId(SPUIComponetIdProvider.SOFT_MODULE_VERSION); + + /* Vendor text field */ + vendorTextField = SPUIComponentProvider.getTextField("", ValoTheme.TEXTFIELD_TINY, false, null, + i18n.get("textfield.vendor"), true, SPUILabelDefinitions.TEXT_FIELD_MAX_LENGTH); + vendorTextField.setId(SPUIComponetIdProvider.SOFT_MODULE_VENDOR); + + descTextArea = SPUIComponentProvider.getTextArea("text-area-style", ValoTheme.TEXTAREA_TINY, false, null, + i18n.get("textfield.description"), SPUILabelDefinitions.TEXT_AREA_MAX_LENGTH); + descTextArea.setId(SPUIComponetIdProvider.ADD_SW_MODULE_DESCRIPTION); + addDescriptionTextChangeListener(); + addVendorTextChangeListener(); + + /* Label for mandatory symbol */ + madatoryLabel = new Label(i18n.get("label.mandatory.field")); + madatoryLabel.setStyleName(SPUIStyleDefinitions.SP_TEXTFIELD_ERROR); + madatoryLabel.addStyleName(ValoTheme.LABEL_SMALL); + + typeComboBox = SPUIComponentProvider.getComboBox("", "", null, null, false, null, + i18n.get("upload.swmodule.type")); + typeComboBox.setId(SPUIComponetIdProvider.SW_MODULE_TYPE); + typeComboBox.setStyleName(SPUIDefinitions.COMBO_BOX_SPECIFIC_STYLE + " " + ValoTheme.COMBOBOX_TINY); + typeComboBox.setNewItemsAllowed(Boolean.FALSE); + typeComboBox.setImmediate(Boolean.TRUE); + + populateTypeNameCombo(); + + /* save or update button */ + saveSoftware = SPUIComponentProvider.getButton(SPUIComponetIdProvider.SOFT_MODULE_SAVE, "", "", "", true, + FontAwesome.SAVE, SPUIButtonStyleSmallNoBorder.class); + saveSoftware.addClickListener(event -> { + if (editSwModule) { + updateSwModule(); + } else { + /* add new or update software module */ + addNewBaseSoftware(); + } + }); + + /* close button */ + closeWindow = SPUIComponentProvider.getButton(SPUIComponetIdProvider.SOFT_MODULE_DISCARD, "", "", "", true, + FontAwesome.TIMES, SPUIButtonStyleSmallNoBorder.class); + /* Just close this window when this button is clicked */ + closeWindow.addClickListener(event -> closeThisWindow()); + + resetOldValues(); + } + + /** + * + */ + private void populateTypeNameCombo() { + typeComboBox.setContainerDataSource(HawkbitCommonUtil.createLazyQueryContainer( + new BeanQueryFactory(SoftwareModuleTypeBeanQuery.class))); + typeComboBox.setItemCaptionPropertyId(SPUILabelDefinitions.VAR_NAME); + + } + + private void resetOldValues() { + oldDescriptionValue = null; + oldVendorValue = null; + } + + /** + * Keep UI components on Layout. + * + * @return + */ + private void createWindow() { + /* action button layout (save & dicard) */ + final HorizontalLayout buttonsLayout = new HorizontalLayout(); + buttonsLayout.setSizeFull(); + buttonsLayout.addComponents(saveSoftware, closeWindow); + buttonsLayout.setComponentAlignment(saveSoftware, Alignment.BOTTOM_LEFT); + buttonsLayout.setComponentAlignment(closeWindow, Alignment.BOTTOM_RIGHT); + buttonsLayout.addStyleName("window-style"); + + final Label madatoryStarLabel = new Label("*"); + madatoryStarLabel.setStyleName("v-caption v-required-field-indicator"); + madatoryStarLabel.setWidth(null); + final HorizontalLayout hLayout = new HorizontalLayout(); + hLayout.setSizeFull(); + hLayout.addComponents(typeComboBox, madatoryStarLabel); + hLayout.setComponentAlignment(typeComboBox, Alignment.TOP_LEFT); + hLayout.setComponentAlignment(madatoryStarLabel, Alignment.TOP_RIGHT); + hLayout.setExpandRatio(typeComboBox, 0.8f); + + /* + * The main layout of the window contains mandatory info, textboxes + * (controller Id, name & description) and action buttons layout + */ + final VerticalLayout mainLayout = new VerticalLayout(); + mainLayout.setSpacing(Boolean.TRUE); + mainLayout.addStyleName("lay-color"); + mainLayout.addComponent(madatoryLabel); + mainLayout.setComponentAlignment(madatoryLabel, Alignment.MIDDLE_LEFT); + mainLayout.addComponent(hLayout); + mainLayout.setComponentAlignment(hLayout, Alignment.MIDDLE_LEFT); + mainLayout.addComponents(nameTextField, versionTextField, vendorTextField, descTextArea, buttonsLayout); + + /* add main layout to the window */ + window = SPUIComponentProvider.getWindow(i18n.get("upload.caption.add.new.swmodule"), null, + SPUIDefinitions.CREATE_UPDATE_WINDOW); + window.setContent(mainLayout); + window.setModal(true); + } + + private void addDescriptionTextChangeListener() { + descTextArea.addTextChangeListener(new TextChangeListener() { + @Override + public void textChange(final TextChangeEvent event) { + if (event.getText().equals(oldDescriptionValue) && vendorTextField.getValue().equals(oldVendorValue)) { + saveSoftware.setEnabled(false); + } else { + saveSoftware.setEnabled(true); + } + } + }); + } + + private void addVendorTextChangeListener() { + vendorTextField.addTextChangeListener(new TextChangeListener() { + @Override + public void textChange(final TextChangeEvent event) { + if (event.getText().equals(oldVendorValue) && descTextArea.getValue().equals(oldDescriptionValue)) { + saveSoftware.setEnabled(false); + } else { + saveSoftware.setEnabled(true); + } + } + }); + } + + /** + * Add new SW module. + */ + private void addNewBaseSoftware() { + final String name = HawkbitCommonUtil.trimAndNullIfEmpty(nameTextField.getValue()); + final String version = HawkbitCommonUtil.trimAndNullIfEmpty(versionTextField.getValue()); + final String vendor = HawkbitCommonUtil.trimAndNullIfEmpty(vendorTextField.getValue()); + final String description = HawkbitCommonUtil.trimAndNullIfEmpty(descTextArea.getValue()); + final String type = typeComboBox.getValue() != null ? typeComboBox.getValue().toString() : null; + if (mandatoryCheck(name, version, type)) { + if (HawkbitCommonUtil.isDuplicate(name, version)) { + uiNotifcation.displayValidationError( + i18n.get("message.duplicate.softwaremodule", new Object[] { name, version })); + } else { + final SoftwareModule newBaseSoftwareModule = HawkbitCommonUtil.addNewBaseSoftware(name, version, vendor, + softwareManagement.findSoftwareModuleTypeByName(type), description); + if (newBaseSoftwareModule != null) { + /* display success message */ + uiNotifcation.displaySuccess(i18n.get("message.save.success", new Object[] { + newBaseSoftwareModule.getName() + ":" + newBaseSoftwareModule.getVersion() })); + eventBus.publish(this, new SoftwareModuleEvent(SoftwareModuleEventType.NEW_SOFTWARE_MODULE, + newBaseSoftwareModule)); + } + // close the window + closeThisWindow(); + } + } + } + + private void updateSwModule() { + final String newDesc = HawkbitCommonUtil.trimAndNullIfEmpty(descTextArea.getValue()); + final String newVendor = HawkbitCommonUtil.trimAndNullIfEmpty(vendorTextField.getValue()); + SoftwareModule newSWModule = softwareManagement.findSoftwareModuleById(baseSwModuleId); + newSWModule.setVendor(newVendor); + newSWModule.setDescription(newDesc); + newSWModule = softwareManagement.updateSoftwareModule(newSWModule); + if (newSWModule != null) { + uiNotifcation.displaySuccess(i18n.get("message.save.success", + new Object[] { newSWModule.getName() + ":" + newSWModule.getVersion() })); + + eventBus.publish(this, + new SoftwareModuleEvent(SoftwareModuleEventType.UPDATED_SOFTWARE_MODULE, newSWModule)); + } + closeThisWindow(); + } + + private void populateValuesOfSwModule() { + final SoftwareModule swModle = softwareManagement.findSoftwareModuleById(baseSwModuleId); + nameTextField.setValue(swModle.getName()); + versionTextField.setValue(swModle.getVersion()); + vendorTextField.setValue(swModle.getVendor() == null ? HawkbitCommonUtil.SP_STRING_EMPTY + : HawkbitCommonUtil.trimAndNullIfEmpty(swModle.getVendor())); + descTextArea.setValue(swModle.getDescription() == null ? HawkbitCommonUtil.SP_STRING_EMPTY + : HawkbitCommonUtil.trimAndNullIfEmpty(swModle.getDescription())); + oldDescriptionValue = descTextArea.getValue(); + oldVendorValue = vendorTextField.getValue(); + if (swModle.getType().isDeleted()) { + typeComboBox.addItem(swModle.getType().getName()); + } + typeComboBox.setValue(swModle.getType().getName()); + saveSoftware.setEnabled(Boolean.FALSE); + } + + /** + * Method to close window. + */ + private void closeThisWindow() { + window.close(); + UI.getCurrent().removeWindow(window); + } + + /** + * Validation check - Mandatory. + * + * @param name + * as String + * @param version + * as version + * @return boolena as flag + */ + private boolean mandatoryCheck(final String name, final String version, final String type) { + boolean isValid = true; + if (name == null || version == null || type == null) { + if (name == null) { + nameTextField.addStyleName(SPUIStyleDefinitions.SP_TEXTFIELD_ERROR); + } + if (version == null) { + versionTextField.addStyleName(SPUIStyleDefinitions.SP_TEXTFIELD_ERROR); + } + if (type == null) { + typeComboBox.addStyleName(SPUIStyleDefinitions.SP_COMBOFIELD_ERROR); + } + + uiNotifcation.displayValidationError(i18n.get("message.mandatory.check")); + isValid = false; + } + return isValid; + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleDetails.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleDetails.java new file mode 100755 index 000000000..674152164 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleDetails.java @@ -0,0 +1,283 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.smtable; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.eclipse.hawkbit.repository.SpPermissionChecker; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleEvent; +import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleEvent.SoftwareModuleEventType; +import org.eclipse.hawkbit.ui.artifacts.state.ArtifactUploadState; +import org.eclipse.hawkbit.ui.common.detailslayout.AbstractTableDetailsLayout; +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPUIComponetIdProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.vaadin.spring.events.EventBus; +import org.vaadin.spring.events.EventScope; +import org.vaadin.spring.events.annotation.EventBusListenerMethod; + +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.Label; +import com.vaadin.ui.TabSheet; +import com.vaadin.ui.UI; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; + +/** + * Software module details. + * + * + */ +@SpringComponent +@ViewScope +public class SoftwareModuleDetails extends AbstractTableDetailsLayout { + + private static final long serialVersionUID = -4900381301076646366L; + + @Autowired + private I18N i18n; + + @Autowired + private transient EventBus.SessionEventBus eventBus; + + @Autowired + private SpPermissionChecker permissionChecker; + + @Autowired + private SoftwareModuleAddUpdateWindow softwareModuleAddUpdateWindow; + + @Autowired + private ArtifactUploadState artifactUploadState; + + private UI ui; + + private Long swModuleId; + + private SoftwareModule selectedSwModule; + + /** + * Initialize the component. + */ + @PostConstruct + protected void init() { + super.init(); + ui = UI.getCurrent(); + eventBus.subscribe(this); + } + + @Override + protected String getEditButtonId() { + return SPUIComponetIdProvider.UPLOAD_SW_MODULE_EDIT_BUTTON; + } + + @Override + protected void addTabs(final TabSheet detailsTab) { + detailsTab.addTab(createDetailsLayout(), i18n.get("caption.tab.details"), null); + detailsTab.addTab(createDescriptionLayout(), i18n.get("caption.tab.description"), null); + detailsTab.addTab(createLogLayout(), i18n.get("caption.logs.tab"), null); + } + + @Override + protected void onEdit(final ClickEvent event) { + final Window addSoftwareModule = softwareModuleAddUpdateWindow.createUpdateSoftwareModuleWindow(swModuleId); + addSoftwareModule.setCaption(i18n.get("upload.caption.update.swmodule")); + UI.getCurrent().addWindow(addSoftwareModule); + addSoftwareModule.setVisible(Boolean.TRUE); + } + + private void populateDetails(final SoftwareModule swModule) { + String maxAssign = HawkbitCommonUtil.SP_STRING_EMPTY; + if (swModule != null) { + if (swModule.getType().getMaxAssignments() == Integer.MAX_VALUE) { + maxAssign = i18n.get("label.multiAssign.type"); + } else { + maxAssign = i18n.get("label.singleAssign.type"); + } + updateSoftwareModuleDetailsLayout(swModule.getType().getName(), swModule.getVendor(), maxAssign); + } else { + updateSoftwareModuleDetailsLayout(HawkbitCommonUtil.SP_STRING_EMPTY, HawkbitCommonUtil.SP_STRING_EMPTY, + maxAssign); + } + } + + private void updateSoftwareModuleDetailsLayout(final String type, final String vendor, final String maxAssign) { + final VerticalLayout detailsTabLayout = getDetailsLayout(); + + detailsTabLayout.removeAllComponents(); + + final Label vendorLabel = SPUIComponentProvider.createNameValueLabel(i18n.get("label.dist.details.vendor"), + HawkbitCommonUtil.trimAndNullIfEmpty(vendor) == null ? "" : vendor); + vendorLabel.setId(SPUIComponetIdProvider.DETAILS_VENDOR_LABEL_ID); + detailsTabLayout.addComponent(vendorLabel); + + if (type != null) { + final Label typeLabel = SPUIComponentProvider.createNameValueLabel(i18n.get("label.dist.details.type"), + type); + typeLabel.setId(SPUIComponetIdProvider.DETAILS_TYPE_LABEL_ID); + detailsTabLayout.addComponent(typeLabel); + } + + final Label assignLabel = SPUIComponentProvider.createNameValueLabel(i18n.get("label.assigned.type"), + HawkbitCommonUtil.trimAndNullIfEmpty(maxAssign) == null ? "" : maxAssign); + assignLabel.setId(SPUIComponetIdProvider.SWM_DTLS_MAX_ASSIGN); + detailsTabLayout.addComponent(assignLabel); + + } + + private void populateLog(final SoftwareModule softwareModule) { + if (null != softwareModule) { + updateLogLayout(getLogLayout(), softwareModule.getLastModifiedAt(), softwareModule.getLastModifiedBy(), + softwareModule.getCreatedAt(), softwareModule.getCreatedBy(), i18n); + } else { + updateLogLayout(getLogLayout(), null, HawkbitCommonUtil.SP_STRING_EMPTY, null, null, i18n); + } + } + + public void setSwModuleId(final Long swModuleId) { + this.swModuleId = swModuleId; + } + + private void populateDetailsWidget(final SoftwareModule swModule) { + if (swModule != null) { + setSwModuleId(swModule.getId()); + setName(getDefaultCaption(), + HawkbitCommonUtil.getFormattedNameVersion(swModule.getName(), swModule.getVersion())); + populateDetails(swModule); + populateDescription(swModule); + populateLog(swModule); + } else { + setSwModuleId(null); + setName(getDefaultCaption(), HawkbitCommonUtil.SP_STRING_EMPTY); + populateDetails(null); + populateDescription(null); + populateLog(null); + } + } + + private void populateDescription(final SoftwareModule swModule) { + if (swModule != null) { + updateDescriptionLayout(i18n.get("label.description"), swModule.getDescription()); + } else { + updateDescriptionLayout(i18n.get("label.description"), null); + } + } + + @PreDestroy + void destroy() { + /* + * It's good manners to do this, even though vaadin-spring will + * automatically unsubscribe when this UI is garbage collected. + */ + eventBus.unsubscribe(this); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.detailslayout.TableDetailsLayout# + * getDefaultCaption() + */ + @Override + protected String getDefaultCaption() { + return i18n.get("upload.swModuleTable.header"); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.detailslayout.TableDetailsLayout# + * onLoadIsSwModuleSelected() + */ + @Override + protected Boolean onLoadIsTableRowSelected() { + return artifactUploadState.getSelectedBaseSoftwareModule().isPresent(); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.detailslayout.TableDetailsLayout# + * onLoadIsTableMaximized() + */ + @Override + protected Boolean onLoadIsTableMaximized() { + return artifactUploadState.isSwModuleTableMaximized(); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.detailslayout.TableDetailsLayout# + * populateDetailsWidget() + */ + @Override + protected void populateDetailsWidget() { + populateDetailsWidget(selectedSwModule); + } + + @EventBusListenerMethod(scope = EventScope.SESSION) + void onEvent(final SoftwareModuleEvent softwareModuleEvent) { + if (softwareModuleEvent.getSoftwareModuleEventType() == SoftwareModuleEventType.SELECTED_SOFTWARE_MODULE + || softwareModuleEvent + .getSoftwareModuleEventType() == SoftwareModuleEventType.UPDATED_SOFTWARE_MODULE) { + ui.access(() -> { + /** + * softwareModuleEvent.getSoftwareModule() is null when table + * has no data. + */ + if (softwareModuleEvent.getSoftwareModule() != null) { + selectedSwModule = softwareModuleEvent.getSoftwareModule(); + populateData(true); + } else { + populateData(false); + } + }); + } else if (softwareModuleEvent.getSoftwareModuleEventType() == SoftwareModuleEventType.MINIMIZED) { + showLayout(); + } else if (softwareModuleEvent.getSoftwareModuleEventType() == SoftwareModuleEventType.MAXIMIZED) { + hideLayout(); + } + } + + @Override + protected void clearDetails() { + populateDetailsWidget(null); + } + + @Override + protected Boolean hasEditPermission() { + return permissionChecker.hasUpdateDistributionPermission(); + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.common.detailslayout.AbstractTableDetailsLayout# + * getTabSheetId() + */ + @Override + protected String getTabSheetId() { + return null; + } + + @Override + protected String getDetailsHeaderCaptionId() { + return SPUIComponetIdProvider.TARGET_DETAILS_HEADER_LABEL_ID; + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleTable.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleTable.java new file mode 100755 index 000000000..5d63ed3ab --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleTable.java @@ -0,0 +1,319 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.smtable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.ui.artifacts.event.SMFilterEvent; +import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleEvent; +import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleEvent.SoftwareModuleEventType; +import org.eclipse.hawkbit.ui.artifacts.event.UploadArtifactUIEvent; +import org.eclipse.hawkbit.ui.artifacts.event.UploadViewAcceptCriteria; +import org.eclipse.hawkbit.ui.artifacts.state.ArtifactUploadState; +import org.eclipse.hawkbit.ui.common.table.AbstractTable; +import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPDateTimeUtil; +import org.eclipse.hawkbit.ui.utils.SPUIComponetIdProvider; +import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; +import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; +import org.eclipse.hawkbit.ui.utils.TableColumn; +import org.springframework.beans.factory.annotation.Autowired; +import org.vaadin.addons.lazyquerycontainer.BeanQueryFactory; +import org.vaadin.addons.lazyquerycontainer.LazyQueryContainer; +import org.vaadin.addons.lazyquerycontainer.LazyQueryDefinition; +import org.vaadin.spring.events.EventBus; +import org.vaadin.spring.events.EventScope; +import org.vaadin.spring.events.annotation.EventBusListenerMethod; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.DropHandler; +import com.vaadin.event.dd.acceptcriteria.AcceptCriterion; +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; +import com.vaadin.ui.UI; + +/** + * Header of Software module table. + * + * + * + */ +@SpringComponent +@ViewScope +public class SoftwareModuleTable extends AbstractTable { + + private static final long serialVersionUID = 6469417305487144809L; + + @Autowired + private I18N i18n; + + @Autowired + private ArtifactUploadState artifactUploadState; + + @Autowired + private transient EventBus.SessionEventBus eventBus; + + @Autowired + private transient SoftwareManagement softwareManagement; + + @Autowired + private UploadViewAcceptCriteria uploadViewAcceptCriteria; + + /** + * Initialize the filter layout. + */ + @PostConstruct + protected void init() { + super.init(); + eventBus.subscribe(this); + setNoDataAvailable(); + } + + @PreDestroy + void destroy() { + /* + * It's good manners to do this, even though vaadin-spring will + * automatically unsubscribe when this UI is garbage collected. + */ + eventBus.unsubscribe(this); + } + + @EventBusListenerMethod(scope = EventScope.SESSION) + void onEvent(final SMFilterEvent filterEvent) { + UI.getCurrent().access(() -> { + + if (filterEvent == SMFilterEvent.FILTER_BY_TYPE || filterEvent == SMFilterEvent.FILTER_BY_TEXT + || filterEvent == SMFilterEvent.REMOVER_FILTER_BY_TYPE + || filterEvent == SMFilterEvent.REMOVER_FILTER_BY_TEXT) { + refreshFilter(); + } + }); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.table.SPTable#getTableId() + */ + @Override + protected String getTableId() { + return SPUIComponetIdProvider.UPLOAD_SOFTWARE_MODULE_TABLE; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.table.SPTable#createContainer() + */ + @Override + protected Container createContainer() { + final Map queryConfiguration = new HashMap(); + artifactUploadState.getSoftwareModuleFilters().getSearchText() + .ifPresent(value -> queryConfiguration.put(SPUIDefinitions.FILTER_BY_TEXT, value)); + + artifactUploadState.getSoftwareModuleFilters().getSoftwareModuleType() + .ifPresent(type -> queryConfiguration.put(SPUIDefinitions.BY_SOFTWARE_MODULE_TYPE, type)); + + final BeanQueryFactory swQF = new BeanQueryFactory( + BaseSwModuleBeanQuery.class); + swQF.setQueryConfiguration(queryConfiguration); + + final LazyQueryContainer container = new LazyQueryContainer( + new LazyQueryDefinition(true, SPUIDefinitions.PAGE_SIZE, "swId"), swQF); + return container; + } + + @Override + protected void addContainerProperties(final Container container) { + final LazyQueryContainer lqc = (LazyQueryContainer) container; + lqc.addContainerProperty("nameAndVersion", String.class, null, false, false); + lqc.addContainerProperty(SPUILabelDefinitions.VAR_ID, Long.class, null, false, false); + lqc.addContainerProperty(SPUILabelDefinitions.VAR_DESC, String.class, "", false, true); + lqc.addContainerProperty(SPUILabelDefinitions.VAR_VERSION, String.class, null, false, false); + lqc.addContainerProperty(SPUILabelDefinitions.VAR_NAME, String.class, null, false, true); + lqc.addContainerProperty(SPUILabelDefinitions.VAR_VENDOR, String.class, null, false, true); + lqc.addContainerProperty(SPUILabelDefinitions.VAR_CREATED_BY, String.class, null, false, true); + lqc.addContainerProperty(SPUILabelDefinitions.VAR_LAST_MODIFIED_BY, String.class, null, false, true); + lqc.addContainerProperty(SPUILabelDefinitions.VAR_CREATED_DATE, String.class, null, false, true); + lqc.addContainerProperty(SPUILabelDefinitions.VAR_LAST_MODIFIED_DATE, String.class, null, false, true); + } + + @Override + protected void addCustomGeneratedColumns() { + /* No generated columns */ + } + + @Override + protected boolean isFirstRowSelectedOnLoad() { + return artifactUploadState.getSelectedSoftwareModules().isEmpty(); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.table.SPTable#getItemIdToSelect() + */ + @Override + protected Object getItemIdToSelect() { + return artifactUploadState.getSelectedSoftwareModules(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.table.SPTable#isMaximized() + */ + @Override + protected boolean isMaximized() { + return artifactUploadState.isSwModuleTableMaximized(); + } + + @SuppressWarnings("rawtypes") + @Override + protected void onValueChange() { + eventBus.publish(this, UploadArtifactUIEvent.HIDE_DROP_HINTS); + @SuppressWarnings("unchecked") + final Set values = (Set) getValue(); + if (values != null && !values.isEmpty()) { + final Iterator iterator = values.iterator(); + Long value = null; + while (iterator.hasNext()) { + value = iterator.next(); + } + if (null != value) { + artifactUploadState.setSelectedBaseSwModuleId(value); + final SoftwareModule baseSoftwareModule = softwareManagement.findSoftwareModuleById(value); + artifactUploadState.setSelectedBaseSoftwareModule(baseSoftwareModule); + artifactUploadState.setSelectedSoftwareModules(values); + eventBus.publish(this, + new SoftwareModuleEvent(SoftwareModuleEventType.SELECTED_SOFTWARE_MODULE, baseSoftwareModule)); + } + } else { + artifactUploadState.setSelectedBaseSwModuleId(null); + artifactUploadState.setSelectedBaseSoftwareModule(null); + artifactUploadState.setSelectedSoftwareModules(Collections.emptySet()); + eventBus.publish(this, new SoftwareModuleEvent(SoftwareModuleEventType.SELECTED_SOFTWARE_MODULE, null)); + } + } + + @EventBusListenerMethod(scope = EventScope.SESSION) + void onEvent(final SoftwareModuleEvent event) { + if (event.getSoftwareModuleEventType() == SoftwareModuleEventType.MINIMIZED) { + UI.getCurrent().access(() -> applyMinTableSettings()); + } else if (event.getSoftwareModuleEventType() == SoftwareModuleEventType.MAXIMIZED) { + UI.getCurrent().access(() -> applyMaxTableSettings()); + } else if (event.getSoftwareModuleEventType() == SoftwareModuleEventType.NEW_SOFTWARE_MODULE) { + UI.getCurrent().access(() -> addSoftwareModule(event.getSoftwareModule())); + } + } + + @EventBusListenerMethod(scope = EventScope.SESSION) + void onEvent(final UploadArtifactUIEvent event) { + if (event == UploadArtifactUIEvent.DELETED_ALL_SOFWARE) { + UI.getCurrent().access(() -> refreshFilter()); + } + } + + /** + * Add new software module to table. + * + * @param swModule + * new software module + */ + @SuppressWarnings("unchecked") + private void addSoftwareModule(final SoftwareModule swModule) { + final Object addItem = addItem(); + final Item item = getItem(addItem); + final String swNameVersion = HawkbitCommonUtil.concatStrings(":", swModule.getName(), swModule.getVersion()); + item.getItemProperty(SPUILabelDefinitions.NAME_VERSION).setValue(swNameVersion); + item.getItemProperty("swId").setValue(swModule.getId()); + item.getItemProperty(SPUILabelDefinitions.VAR_ID).setValue(swModule.getId()); + item.getItemProperty(SPUILabelDefinitions.VAR_DESC).setValue(swModule.getDescription()); + item.getItemProperty(SPUILabelDefinitions.VAR_VERSION).setValue(swModule.getVersion()); + item.getItemProperty(SPUILabelDefinitions.VAR_NAME).setValue(swModule.getName()); + item.getItemProperty(SPUILabelDefinitions.VAR_VENDOR).setValue(swModule.getVendor()); + item.getItemProperty(SPUILabelDefinitions.VAR_CREATED_BY).setValue(swModule.getCreatedBy()); + item.getItemProperty(SPUILabelDefinitions.VAR_LAST_MODIFIED_BY).setValue(swModule.getLastModifiedBy()); + item.getItemProperty(SPUILabelDefinitions.VAR_CREATED_DATE) + .setValue(SPDateTimeUtil.getFormattedDate(swModule.getCreatedAt())); + item.getItemProperty(SPUILabelDefinitions.VAR_LAST_MODIFIED_DATE) + .setValue(SPDateTimeUtil.getFormattedDate(swModule.getLastModifiedAt())); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.table.SPTable#getTableVisibleColumns + * () + */ + @Override + protected List getTableVisibleColumns() { + final List columnList = new ArrayList(); + if (isMaximized()) { + columnList.add(new TableColumn(SPUILabelDefinitions.VAR_NAME, i18n.get("header.name"), 0.2f)); + columnList.add(new TableColumn(SPUILabelDefinitions.VAR_VERSION, i18n.get("header.version"), 0.1f)); + columnList.add(new TableColumn(SPUILabelDefinitions.VAR_VENDOR, i18n.get("header.vendor"), 0.1f)); + columnList.add(new TableColumn(SPUILabelDefinitions.VAR_CREATED_BY, i18n.get("header.createdBy"), 0.1f)); + columnList + .add(new TableColumn(SPUILabelDefinitions.VAR_CREATED_DATE, i18n.get("header.createdDate"), 0.1f)); + columnList.add( + new TableColumn(SPUILabelDefinitions.VAR_LAST_MODIFIED_BY, i18n.get("header.modifiedBy"), 0.1f)); + columnList.add(new TableColumn(SPUILabelDefinitions.VAR_LAST_MODIFIED_DATE, i18n.get("header.modifiedDate"), + 0.1f)); + columnList.add(new TableColumn(SPUILabelDefinitions.VAR_DESC, i18n.get("header.description"), 0.2f)); + } else { + columnList.add(new TableColumn(SPUILabelDefinitions.VAR_NAME, i18n.get("header.name"), 0.8f)); + columnList.add(new TableColumn(SPUILabelDefinitions.VAR_VERSION, i18n.get("header.version"), 0.2f)); + } + return columnList; + } + + @Override + protected DropHandler getTableDropHandler() { + return new DropHandler() { + + private static final long serialVersionUID = 1L; + + @Override + public AcceptCriterion getAcceptCriterion() { + return uploadViewAcceptCriteria; + } + + @Override + public void drop(final DragAndDropEvent event) { + /* Not required */ + } + }; + } + + private void setNoDataAvailable() { + final int containerSize = getContainerDataSource().size(); + if (containerSize == 0) { + artifactUploadState.setNoDataAvilableSoftwareModule(true); + } else { + artifactUploadState.setNoDataAvilableSoftwareModule(false); + } + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleTableHeader.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleTableHeader.java new file mode 100755 index 000000000..5725d9203 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleTableHeader.java @@ -0,0 +1,371 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.smtable; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.eclipse.hawkbit.repository.SpPermissionChecker; +import org.eclipse.hawkbit.ui.artifacts.event.SMFilterEvent; +import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleEvent; +import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleEvent.SoftwareModuleEventType; +import org.eclipse.hawkbit.ui.artifacts.event.UploadArtifactUIEvent; +import org.eclipse.hawkbit.ui.artifacts.state.ArtifactUploadState; +import org.eclipse.hawkbit.ui.common.table.AbstractTableHeader; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPUIComponetIdProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.vaadin.spring.events.EventBus; +import org.vaadin.spring.events.EventScope; +import org.vaadin.spring.events.annotation.EventBusListenerMethod; + +import com.vaadin.event.dd.DropHandler; +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.UI; +import com.vaadin.ui.Window; + +/** + * Header of Software module table. + * + * + * + */ +@SpringComponent +@ViewScope +public class SoftwareModuleTableHeader extends AbstractTableHeader { + + private static final long serialVersionUID = 242961845006626297L; + + @Autowired + private I18N i18n; + + @Autowired + private SpPermissionChecker permChecker; + + @Autowired + private transient EventBus.SessionEventBus eventbus; + + @Autowired + private ArtifactUploadState artifactUploadState; + + @Autowired + private SoftwareModuleAddUpdateWindow softwareModuleAddUpdateWindow; + + /** + * Initialize the components. + */ + @PostConstruct + protected void init() { + super.init(); + eventbus.subscribe(this); + } + + @EventBusListenerMethod(scope = EventScope.SESSION) + void onEvent(final UploadArtifactUIEvent event) { + if (event == UploadArtifactUIEvent.HIDE_FILTER_BY_TYPE) { + setFilterButtonsIconVisible(true); + } + } + + @PreDestroy + void destroy() { + /* + * It's good manners to do this, even though vaadin-spring will + * automatically unsubscribe when this UI is garbage collected. + */ + eventbus.unsubscribe(this); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.table.SPTableHeader#getHeaderCaption + * () + */ + @Override + protected String getHeaderCaption() { + return i18n.get("upload.swModuleTable.header"); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.table.SPTableHeader#getSearchBoxId() + */ + @Override + protected String getSearchBoxId() { + return SPUIComponetIdProvider.SW_MODULE_SEARCH_TEXT_FIELD; + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.common.table.SPTableHeader#getSearchRestIconId() + */ + @Override + protected String getSearchRestIconId() { + return SPUIComponetIdProvider.SW_MODULE_SEARCH_RESET_ICON; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.table.SPTableHeader#getAddIconId() + */ + @Override + protected String getAddIconId() { + return SPUIComponetIdProvider.SW_MODULE_ADD_BUTTON; + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.common.table.SPTableHeader#onLoadSearchBoxValue() + */ + @Override + protected String onLoadSearchBoxValue() { + if (artifactUploadState.getSoftwareModuleFilters().getSearchText().isPresent()) { + return artifactUploadState.getSoftwareModuleFilters().getSearchText().get(); + } + return null; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.table.SPTableHeader#getDropFilterId( + * ) + */ + @Override + protected String getDropFilterId() { + /* No dropping on software module table header in Upload View */ + return null; + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.common.table.SPTableHeader#hasCreatePermission() + */ + @Override + protected boolean hasCreatePermission() { + return permChecker.hasCreateDistributionPermission(); + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.common.table.SPTableHeader#isDropHintRequired() + */ + @Override + protected boolean isDropHintRequired() { + /* No dropping on software module table header in Upload View */ + return false; + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.common.table.SPTableHeader#isDropFilterRequired() + */ + @Override + protected boolean isDropFilterRequired() { + /* No dropping on software module table header in Upload View */ + return false; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.table.SPTableHeader# + * getShowFilterButtonLayoutId() + */ + @Override + protected String getShowFilterButtonLayoutId() { + return "show.type.icon"; + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.common.table.SPTableHeader#showFilterButtonsLayout + * () + */ + @Override + protected void showFilterButtonsLayout() { + artifactUploadState.setSwTypeFilterClosed(false); + eventbus.publish(this, UploadArtifactUIEvent.SHOW_FILTER_BY_TYPE); + + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.table.SPTableHeader#resetSearchText( + * ) + */ + @Override + protected void resetSearchText() { + artifactUploadState.getSoftwareModuleFilters().setSearchText(null); + eventbus.publish(this, SMFilterEvent.REMOVER_FILTER_BY_TEXT); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.table.SPTableHeader#getMaxMinIconId( + * ) + */ + @Override + protected String getMaxMinIconId() { + return SPUIComponetIdProvider.SW_MAX_MIN_TABLE_ICON; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.table.SPTableHeader#maximizeTable() + */ + @Override + public void maximizeTable() { + artifactUploadState.setSwModuleTableMaximized(Boolean.TRUE); + eventbus.publish(this, new SoftwareModuleEvent(SoftwareModuleEventType.MAXIMIZED, null)); + + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.table.SPTableHeader#minimizeTable() + */ + @Override + public void minimizeTable() { + artifactUploadState.setSwModuleTableMaximized(Boolean.FALSE); + eventbus.publish(this, new SoftwareModuleEvent(SoftwareModuleEventType.MINIMIZED, null)); + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.common.table.SPTableHeader#onLoadIsTableMaximized( + * ) + */ + @Override + public Boolean onLoadIsTableMaximized() { + return artifactUploadState.isSwModuleTableMaximized(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.table.SPTableHeader# + * onLoadIsShowFilterButtonDisplayed() + */ + @Override + public Boolean onLoadIsShowFilterButtonDisplayed() { + return artifactUploadState.isSwTypeFilterClosed(); + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.common.table.SPTableHeader#searchBy(java.lang. + * String) + */ + @Override + protected void searchBy(final String newSearchText) { + artifactUploadState.getSoftwareModuleFilters().setSearchText(newSearchText); + eventbus.publish(this, SMFilterEvent.FILTER_BY_TEXT); + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.common.table.SPTableHeader#addNewItem(com.vaadin. + * ui.Button.ClickEvent) + */ + @Override + protected void addNewItem(final ClickEvent event) { + final Window addSoftwareModule = softwareModuleAddUpdateWindow.createAddSoftwareModuleWindow(); + addSoftwareModule.setCaption(i18n.get("upload.caption.add.new.swmodule")); + UI.getCurrent().addWindow(addSoftwareModule); + addSoftwareModule.setVisible(Boolean.TRUE); + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.common.table.AbstractTableHeader#canAddNewItem() + */ + @Override + protected Boolean isAddNewItemAllowed() { + return Boolean.TRUE; + } + + @Override + protected String getFilterIconStyle() { + return null; + } + + @Override + protected String getDropFilterWrapperId() { + return null; + } + + @Override + protected DropHandler getDropFilterHandler() { + return null; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.table.AbstractTableHeader# + * getBulkUploadIconId() + */ + @Override + protected String getBulkUploadIconId() { + return null; + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.common.table.AbstractTableHeader#bulkUpload(com. + * vaadin.ui.Button.ClickEvent ) + */ + @Override + protected void bulkUpload(final ClickEvent event) { + /** + * No implementation as no bulk upload is supported. + */ + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.table.AbstractTableHeader# + * isBulkUploadAllowed() + */ + @Override + protected Boolean isBulkUploadAllowed() { + return Boolean.FALSE; + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleTableLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleTableLayout.java new file mode 100755 index 000000000..8b8d5b7b6 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleTableLayout.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.smtable; + +import javax.annotation.PostConstruct; + +import org.eclipse.hawkbit.ui.common.table.AbstractTableLayout; +import org.springframework.beans.factory.annotation.Autowired; + +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; + +/** + * Software module table layout. + * + * + * + */ +@SpringComponent +@ViewScope +public class SoftwareModuleTableLayout extends AbstractTableLayout { + + private static final long serialVersionUID = 6464291374980641235L; + + @Autowired + private SoftwareModuleDetails softwareModuleDetails; + + @Autowired + private SoftwareModuleTableHeader smTableHeader; + + @Autowired + private SoftwareModuleTable smTable; + + /** + * Initialize the filter layout. + */ + @PostConstruct + void init() { + super.init(smTableHeader, smTable, softwareModuleDetails); + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtype/CreateUpdateSoftwareTypeLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtype/CreateUpdateSoftwareTypeLayout.java new file mode 100755 index 000000000..824faa83b --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtype/CreateUpdateSoftwareTypeLayout.java @@ -0,0 +1,847 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.smtype; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.repository.SpPermissionChecker; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleTypeEvent; +import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleTypeEvent.SoftwareModuleTypeEnum; +import org.eclipse.hawkbit.ui.common.CoordinatesToColor; +import org.eclipse.hawkbit.ui.common.SoftwareModuleTypeBeanQuery; +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleSmallNoBorder; +import org.eclipse.hawkbit.ui.management.tag.SpColorPickerPreview; +import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPUIComponetIdProvider; +import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; +import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; +import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; +import org.eclipse.hawkbit.ui.utils.UINotification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.vaadin.addons.lazyquerycontainer.BeanQueryFactory; +import org.vaadin.spring.events.EventBus; + +import com.google.common.base.Strings; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.server.FontAwesome; +import com.vaadin.server.Page; +import com.vaadin.shared.ui.colorpicker.Color; +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; +import com.vaadin.ui.AbstractColorPicker.Coordinates2Color; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.Button; +import com.vaadin.ui.ComboBox; +import com.vaadin.ui.CustomComponent; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.OptionGroup; +import com.vaadin.ui.Slider; +import com.vaadin.ui.Slider.ValueOutOfBoundsException; +import com.vaadin.ui.TextArea; +import com.vaadin.ui.TextField; +import com.vaadin.ui.UI; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; +import com.vaadin.ui.components.colorpicker.ColorChangeEvent; +import com.vaadin.ui.components.colorpicker.ColorChangeListener; +import com.vaadin.ui.components.colorpicker.ColorPickerGradient; +import com.vaadin.ui.components.colorpicker.ColorSelector; +import com.vaadin.ui.themes.ValoTheme; + +/** + * + * + */ +@SpringComponent +@ViewScope +public class CreateUpdateSoftwareTypeLayout extends CustomComponent implements ColorChangeListener, ColorSelector { + + private static final long serialVersionUID = -5169398523815919367L; + private static final Logger LOG = LoggerFactory.getLogger(CreateUpdateSoftwareTypeLayout.class); + + @Autowired + private SpPermissionChecker permChecker; + + @Autowired + private I18N i18n; + + /** + * Instance of ColorPickerPreview. + */ + private SpColorPickerPreview selPreview; + + @Autowired + private transient UINotification uiNotification; + + @Autowired + private transient SoftwareManagement swTypeManagementService; + + @Autowired + private transient EventBus.SessionEventBus eventBus; + + private String createTypeStr; + private String updateTypeStr; + private String singleAssignStr; + private String multiAssignStr; + private Label createType; + private Label updateType; + private Label singleAssign; + private Label multiAssign; + private Label comboLabel; + private Label colorLabel; + private Label madatoryLabel; + private TextField typeName; + private TextField typeKey; + private TextArea typeDesc; + private Button saveTag; + private Button discardTag; + private Button tagColorPreviewBtn; + private OptionGroup createOptiongroup; + private OptionGroup assignOptiongroup; + private ComboBox typeNameComboBox; + protected static final String DEFAULT_COLOR = "rgb(44,151,32)"; + private static final String TYPE_NAME_DYNAMIC_STYLE = "new-tag-name"; + private static final String TYPE_DESC_DYNAMIC_STYLE = "new-tag-desc"; + private static final String TAG_DYNAMIC_STYLE = "tag-color-preview"; + private Set selectors; + private Color selectedColor; + private ColorPickerGradient colorSelect; + private Slider redSlider; + private Slider greenSlider; + private Slider blueSlider; + private Window swTypeWindow; + protected boolean tagPreviewBtnClicked = false; + private VerticalLayout comboLayout; + private VerticalLayout sliders; + private VerticalLayout colorPickerLayout; + private HorizontalLayout mainLayout; + private VerticalLayout fieldLayout; + + /** RGB color converter. */ + private final Coordinates2Color rgbConverter = new CoordinatesToColor(); + + /** + * Initialize the artifact details layout. + */ + public void init() { + createComponents(); + buildLayout(); + addListeners(); + } + + private void createComponents() { + createTypeStr = i18n.get("label.create.type"); + updateTypeStr = i18n.get("label.update.type"); + singleAssignStr = i18n.get("label.singleAssign.type"); + multiAssignStr = i18n.get("label.multiAssign.type"); + createType = SPUIComponentProvider.getLabel(createTypeStr, null); + updateType = SPUIComponentProvider.getLabel(updateTypeStr, null); + singleAssign = SPUIComponentProvider.getLabel(singleAssignStr, null); + multiAssign = SPUIComponentProvider.getLabel(multiAssignStr, null); + comboLabel = SPUIComponentProvider.getLabel(i18n.get("label.choose.type"), null); + madatoryLabel = getMandatoryLabel(); + colorLabel = SPUIComponentProvider.getLabel(i18n.get("label.choose.type.color"), null); + colorLabel.addStyleName(SPUIDefinitions.COLOR_LABEL_STYLE); + + typeName = SPUIComponentProvider.getTextField("", ValoTheme.TEXTFIELD_TINY + " " + SPUIDefinitions.TYPE_NAME, + true, "", i18n.get("textfield.name"), true, SPUILabelDefinitions.TEXT_FIELD_MAX_LENGTH); + typeName.setId(SPUIDefinitions.NEW_SOFTWARE_TYPE_NAME); + + typeKey = SPUIComponentProvider.getTextField("", ValoTheme.TEXTFIELD_TINY + " " + SPUIDefinitions.TYPE_KEY, + true, "", i18n.get("textfield.key"), true, SPUILabelDefinitions.TEXT_FIELD_MAX_LENGTH); + typeKey.setId(SPUIDefinitions.NEW_SOFTWARE_TYPE_KEY); + + typeDesc = SPUIComponentProvider.getTextArea("", ValoTheme.TEXTFIELD_TINY + " " + SPUIDefinitions.TYPE_DESC, + false, "", i18n.get("textfield.description"), SPUILabelDefinitions.TEXT_AREA_MAX_LENGTH); + + typeDesc.setId(SPUIDefinitions.NEW_SOFTWARE_TYPE_DESC); + typeDesc.setImmediate(true); + typeDesc.setNullRepresentation(""); + + typeNameComboBox = SPUIComponentProvider.getComboBox("", "", null, null, false, "", + i18n.get("label.combobox.type")); + typeNameComboBox.addStyleName(SPUIDefinitions.FILTER_TYPE_COMBO_STYLE); + typeNameComboBox.setImmediate(true); + + saveTag = SPUIComponentProvider.getButton(SPUIDefinitions.NEW_SW_TYPE_SAVE, "", "", "", true, FontAwesome.SAVE, + SPUIButtonStyleSmallNoBorder.class); + saveTag.addStyleName(ValoTheme.BUTTON_BORDERLESS); + + discardTag = SPUIComponentProvider.getButton(SPUIDefinitions.NEW_TARGET_TAG_DISRACD, "", "", + "discard-button-style", true, FontAwesome.TIMES, SPUIButtonStyleSmallNoBorder.class); + discardTag.addStyleName(ValoTheme.BUTTON_BORDERLESS); + + tagColorPreviewBtn = new Button(); + tagColorPreviewBtn.setId(SPUIComponetIdProvider.TAG_COLOR_PREVIEW_ID); + getPreviewButtonColor(DEFAULT_COLOR); + tagColorPreviewBtn.setStyleName(TAG_DYNAMIC_STYLE); + + selectors = new HashSet(); + selectedColor = new Color(44, 151, 32); + selPreview = new SpColorPickerPreview(selectedColor); + + colorSelect = new ColorPickerGradient("rgb-gradient", rgbConverter); + colorSelect.setColor(selectedColor); + colorSelect.setWidth("220px"); + + redSlider = createRGBSlider("", "red"); + greenSlider = createRGBSlider("", "green"); + blueSlider = createRGBSlider("", "blue"); + setRgbSliderValues(selectedColor); + + createUpdateOptionGroup(); + + singleMultiOptionGroup(); + + } + + private void buildLayout() { + comboLayout = new VerticalLayout(); + + sliders = new VerticalLayout(); + sliders.addComponents(redSlider, greenSlider, blueSlider); + + selectors.add(colorSelect); + + colorPickerLayout = new VerticalLayout(); + colorPickerLayout.setStyleName("rgb-vertical-layout"); + colorPickerLayout.addComponent(selPreview); + colorPickerLayout.addComponent(colorSelect); + + fieldLayout = new VerticalLayout(); + fieldLayout.setSpacing(true); + fieldLayout.setMargin(false); + fieldLayout.setWidth("100%"); + fieldLayout.setHeight(null); + fieldLayout.addComponent(createOptiongroup); + fieldLayout.addComponent(comboLayout); + fieldLayout.addComponent(madatoryLabel); + fieldLayout.addComponent(typeName); + fieldLayout.addComponent(typeKey); + fieldLayout.addComponent(typeDesc); + fieldLayout.addComponent(assignOptiongroup); + + final HorizontalLayout colorLabelLayout = new HorizontalLayout(); + colorLabelLayout.addComponents(colorLabel, tagColorPreviewBtn); + fieldLayout.addComponent(colorLabelLayout); + + final HorizontalLayout buttonLayout = new HorizontalLayout(); + buttonLayout.addComponent(saveTag); + buttonLayout.addComponent(discardTag); + buttonLayout.setComponentAlignment(discardTag, Alignment.BOTTOM_RIGHT); + buttonLayout.setComponentAlignment(saveTag, Alignment.BOTTOM_LEFT); + buttonLayout.addStyleName("window-style"); + buttonLayout.setWidth("152px"); + + final VerticalLayout fieldButtonLayout = new VerticalLayout(); + fieldButtonLayout.addComponent(fieldLayout); + fieldButtonLayout.addComponent(buttonLayout); + + mainLayout = new HorizontalLayout(); + mainLayout.addComponent(fieldButtonLayout); + setCompositionRoot(mainLayout); + } + + private void addListeners() { + saveTag.addClickListener(event -> save()); + discardTag.addClickListener(event -> discard()); + colorSelect.addColorChangeListener(this); + selPreview.addColorChangeListener(this); + tagColorPreviewBtn.addClickListener(event -> previewButtonClicked()); + createOptiongroup.addValueChangeListener(event -> createOptionValueChanged(event)); + typeNameComboBox.addValueChangeListener(event -> typeNameChosen(event)); + slidersValueChangeListeners(); + } + + public Window getWindow() { + reset(); + swTypeWindow = SPUIComponentProvider.getWindow(i18n.get("caption.add.type"), null, + SPUIDefinitions.CREATE_UPDATE_WINDOW); + swTypeWindow.setContent(this); + return swTypeWindow; + + } + + /** + * Listener for option group - Create tag/Update. + * + * @param event + * ValueChangeEvent + */ + private void createOptionValueChanged(final ValueChangeEvent event) { + if ("Update Type".equals(event.getProperty().getValue())) { + typeName.clear(); + typeDesc.clear(); + typeKey.clear(); + typeKey.setEnabled(false); + typeName.setEnabled(false); + assignOptiongroup.setEnabled(false); + + populateTypeNameCombo(); + // show target name combo + comboLayout.addComponent(comboLabel); + comboLayout.addComponent(typeNameComboBox); + } else { + typeKey.setEnabled(true); + typeName.setEnabled(true); + typeName.clear(); + typeDesc.clear(); + typeKey.clear(); + assignOptiongroup.setEnabled(true); + // hide target name combo + comboLayout.removeComponent(comboLabel); + comboLayout.removeComponent(typeNameComboBox); + } + // close the color picker layout + tagPreviewBtnClicked = false; + // reset the selected color - Set defualt color + restoreComponentStyles(); + getPreviewButtonColor(DEFAULT_COLOR); + selPreview.setColor(rgbToColorConverter(DEFAULT_COLOR)); + // remove the sliders and color picker layout + fieldLayout.removeComponent(sliders); + mainLayout.removeComponent(colorPickerLayout); + + } + + /** + * Populate Software Module Type name combo. + */ + public void populateTypeNameCombo() { + typeNameComboBox.setContainerDataSource(HawkbitCommonUtil.createLazyQueryContainer( + new BeanQueryFactory(SoftwareModuleTypeBeanQuery.class))); + typeNameComboBox.setItemCaptionPropertyId(SPUILabelDefinitions.VAR_NAME); + } + + /** + * reset the components. + */ + private void reset() { + typeName.setEnabled(true); + typeName.clear(); + typeKey.clear(); + typeDesc.clear(); + restoreComponentStyles(); + + // hide target name combo + comboLayout.removeComponent(comboLabel); + comboLayout.removeComponent(typeNameComboBox); + fieldLayout.removeComponent(sliders); + mainLayout.removeComponent(colorPickerLayout); + + createOptiongroup.select(createTypeStr); + assignOptiongroup.select(singleAssignStr); + // Default green color + selectedColor = new Color(44, 151, 32); + selPreview.setColor(selectedColor); + tagPreviewBtnClicked = false; + } + + private void typeNameChosen(final ValueChangeEvent event) { + final String tagSelected = (String) event.getProperty().getValue(); + if (null != tagSelected) { + setTypeTagCombo(tagSelected); + } else { + resetTagNameField(); + } + } + + private void resetTagNameField() { + typeName.setEnabled(false); + typeName.clear(); + + typeKey.clear(); + typeDesc.clear(); + restoreComponentStyles(); + fieldLayout.removeComponent(sliders); + mainLayout.removeComponent(colorPickerLayout); + + assignOptiongroup.select(singleAssignStr); + // Default green color + selectedColor = new Color(44, 151, 32); + selPreview.setColor(selectedColor); + tagPreviewBtnClicked = false; + + } + + /** + * Select tag & set tag name & tag desc values corresponding to selected + * tag. + * + * @param targetTagSelected + * as the selected tag from combo + */ + private void setTypeTagCombo(final String targetTagSelected) { + typeName.setValue(targetTagSelected); + final SoftwareModuleType selectedTypeTag = swTypeManagementService + .findSoftwareModuleTypeByName(targetTagSelected); + if (null != selectedTypeTag) { + typeDesc.setValue(selectedTypeTag.getDescription()); + typeKey.setValue(selectedTypeTag.getKey()); + if (selectedTypeTag.getMaxAssignments() == Integer.MAX_VALUE) { + assignOptiongroup.setValue(multiAssignStr); + } else { + assignOptiongroup.setValue(singleAssignStr); + } + + if (null == selectedTypeTag.getColour()) { + selectedColor = new Color(44, 151, 32); + selPreview.setColor(selectedColor); + colorSelect.setColor(selectedColor); + createDynamicStyleForComponents(typeName, typeKey, typeDesc, DEFAULT_COLOR); + getPreviewButtonColor(DEFAULT_COLOR); + } else { + selectedColor = rgbToColorConverter(selectedTypeTag.getColour()); + selPreview.setColor(selectedColor); + colorSelect.setColor(selectedColor); + createDynamicStyleForComponents(typeName, typeKey, typeDesc, selectedTypeTag.getColour()); + getPreviewButtonColor(selectedTypeTag.getColour()); + } + + } + } + + /** + * Dynamic styles for window. + * + * @param top + * int value + * @param marginLeft + * int value + */ + private void getPreviewButtonColor(final String color) { + Page.getCurrent().getJavaScript().execute(HawkbitCommonUtil.getPreviewButtonColorScript(color)); + } + + private void createUpdateOptionGroup() { + final List optionValues = new ArrayList<>(); + if (permChecker.hasCreateDistributionPermission()) { + optionValues.add(createType.getValue()); + } + if (permChecker.hasUpdateDistributionPermission()) { + optionValues.add(updateType.getValue()); + } + createOptionGroup(optionValues); + } + + private void singleMultiOptionGroup() { + final List optionValues = new ArrayList<>(); + optionValues.add(singleAssign.getValue()); + optionValues.add(multiAssign.getValue()); + assignOptionGroup(optionValues); + } + + private void createOptionGroup(final List tagOptions) { + createOptiongroup = new OptionGroup("", tagOptions); + createOptiongroup.setStyleName(ValoTheme.OPTIONGROUP_SMALL); + createOptiongroup.addStyleName("custom-option-group"); + createOptiongroup.setNullSelectionAllowed(false); + if (!tagOptions.isEmpty()) { + createOptiongroup.select(tagOptions.get(0)); + } + } + + private void assignOptionGroup(final List tagOptions) { + assignOptiongroup = new OptionGroup("", tagOptions); + assignOptiongroup.setStyleName(ValoTheme.OPTIONGROUP_SMALL); + assignOptiongroup.addStyleName("custom-option-group"); + assignOptiongroup.setNullSelectionAllowed(false); + assignOptiongroup.select(tagOptions.get(0)); + } + + private Label getMandatoryLabel() { + final Label label = new Label(i18n.get("label.mandatory.field")); + label.setStyleName(SPUIStyleDefinitions.SP_TEXTFIELD_ERROR + " " + ValoTheme.LABEL_SMALL); + return label; + } + + private Slider createRGBSlider(final String caption, final String styleName) { + final Slider slider = new Slider(caption, 0, 255); + slider.setImmediate(true); + slider.setWidth("150px"); + slider.addStyleName(styleName); + return slider; + } + + private void setRgbSliderValues(final Color color) { + try { + final double redColorValue = color.getRed(); + redSlider.setValue(new Double(redColorValue)); + final double blueColorValue = color.getBlue(); + blueSlider.setValue(new Double(blueColorValue)); + final double greenColorValue = color.getGreen(); + greenSlider.setValue(new Double(greenColorValue)); + } catch (final ValueOutOfBoundsException e) { + LOG.error("Unable to set RGB color value to " + color.getRed() + "," + color.getGreen() + "," + + color.getBlue(), e); + } + } + + /** + * Value change listeners implementations of sliders. + */ + private void slidersValueChangeListeners() { + redSlider.addValueChangeListener(new ValueChangeListener() { + private static final long serialVersionUID = -8336732888800920839L; + + @Override + public void valueChange(final ValueChangeEvent event) { + final double red = (Double) event.getProperty().getValue(); + final Color newColor = new Color((int) red, selectedColor.getGreen(), selectedColor.getBlue()); + setColorToComponents(newColor); + } + }); + greenSlider.addValueChangeListener(new ValueChangeListener() { + private static final long serialVersionUID = 1236358037766775663L; + + @Override + public void valueChange(final ValueChangeEvent event) { + final double green = (Double) event.getProperty().getValue(); + final Color newColor = new Color(selectedColor.getRed(), (int) green, selectedColor.getBlue()); + setColorToComponents(newColor); + } + }); + blueSlider.addValueChangeListener(new ValueChangeListener() { + private static final long serialVersionUID = 8466370763686043947L; + + @Override + public void valueChange(final ValueChangeEvent event) { + final double blue = (Double) event.getProperty().getValue(); + final Color newColor = new Color(selectedColor.getRed(), selectedColor.getGreen(), (int) blue); + setColorToComponents(newColor); + } + }); + } + + private void setColorToComponents(final Color newColor) { + setColor(newColor); + colorSelect.setColor(newColor); + getPreviewButtonColor(newColor.getCSS()); + createDynamicStyleForComponents(typeName, typeKey, typeDesc, newColor.getCSS()); + } + + /** + * reset the tag name and tag description component border color. + */ + private void restoreComponentStyles() { + typeName.removeStyleName(TYPE_NAME_DYNAMIC_STYLE); + typeDesc.removeStyleName(TYPE_DESC_DYNAMIC_STYLE); + typeKey.removeStyleName(TYPE_NAME_DYNAMIC_STYLE); + getPreviewButtonColor(DEFAULT_COLOR); + } + + private void save() { + if (mandatoryValuesPresent()) { + final SoftwareModuleType existingType = swTypeManagementService + .findSoftwareModuleTypeByName(typeName.getValue()); + if (createOptiongroup.getValue().equals(createTypeStr)) { + if (!checkIsKeyDuplicate(typeKey.getValue()) && !checkIsDuplicate(existingType)) { + crateNewSWModuleType(); + } + } else { + + updateSWModuleType(existingType); + } + } + } + + private boolean checkIsKeyDuplicate(final String key) { + final SoftwareModuleType existingKeyType = swTypeManagementService.findSoftwareModuleTypeByKey(key); + if (existingKeyType != null) { + uiNotification.displayValidationError( + i18n.get("message.type.key.swmodule.duplicate.check", new Object[] { existingKeyType.getKey() })); + return Boolean.TRUE; + } + return Boolean.FALSE; + } + + private Boolean mandatoryValuesPresent() { + if (Strings.isNullOrEmpty(typeName.getValue()) && Strings.isNullOrEmpty(typeKey.getValue())) { + if (createOptiongroup.getValue().equals(createTypeStr)) { + uiNotification.displayValidationError(SPUILabelDefinitions.MISSING_TYPE_NAME_KEY); + } + if (createOptiongroup.getValue().equals(updateTypeStr)) { + if (null == typeNameComboBox.getValue()) { + uiNotification.displayValidationError(i18n.get("message.error.missing.typename")); + } else { + uiNotification.displayValidationError(SPUILabelDefinitions.MISSING_TAG_NAME); + } + } + return Boolean.FALSE; + } + return Boolean.TRUE; + } + + private Boolean checkIsDuplicate(final SoftwareModuleType existingType) { + if (existingType != null) { + uiNotification.displayValidationError( + i18n.get("message.tag.duplicate.check", new Object[] { existingType.getName() })); + return Boolean.TRUE; + } + return Boolean.FALSE; + } + + private void closeWindow() { + swTypeWindow.close(); + UI.getCurrent().removeWindow(swTypeWindow); + } + + private void discard() { + UI.getCurrent().removeWindow(swTypeWindow); + } + + /** + * Create new tag. + */ + private void crateNewSWModuleType() { + int assignNumber = 0; + final String colorPicked = getColorPickedSting(); + final String typeNameValue = HawkbitCommonUtil.trimAndNullIfEmpty(typeName.getValue()); + final String typeKeyValue = HawkbitCommonUtil.trimAndNullIfEmpty(typeKey.getValue()); + final String typeDescValue = HawkbitCommonUtil.trimAndNullIfEmpty(typeDesc.getValue()); + final String assignValue = (String) assignOptiongroup.getValue(); + if (null != assignValue && assignValue.equalsIgnoreCase(singleAssignStr)) { + assignNumber = 1; + } else if (null != assignValue && assignValue.equalsIgnoreCase(multiAssignStr)) { + assignNumber = Integer.MAX_VALUE; + } + + if (null != typeNameValue && null != typeKeyValue) { + SoftwareModuleType newSWType = new SoftwareModuleType(typeKeyValue, typeNameValue, typeDescValue, + assignNumber, colorPicked); + if (null != typeDescValue) { + newSWType.setDescription(typeDescValue); + } + + newSWType.setColour(colorPicked); + + newSWType = swTypeManagementService.createSoftwareModuleType(newSWType); + uiNotification.displaySuccess(i18n.get("message.save.success", new Object[] { newSWType.getName() })); + closeWindow(); + eventBus.publish(this, + new SoftwareModuleTypeEvent(SoftwareModuleTypeEnum.ADD_SOFTWARE_MODULE_TYPE, newSWType)); + + } else { + uiNotification.displayValidationError(i18n.get("message.error.missing.typenameorkey")); + + } + } + + /** + * Get color picked value in string. + * + * @return String of color picked value. + */ + private String getColorPickedSting() { + return "rgb(" + getSelPreview().getColor().getRed() + "," + getSelPreview().getColor().getGreen() + "," + + getSelPreview().getColor().getBlue() + ")"; + } + + /** + * Color view. + * + * @return ColorPickerPreview as UI + */ + public SpColorPickerPreview getSelPreview() { + return selPreview; + } + + /** + * update tag. + */ + private void updateSWModuleType(final SoftwareModuleType existingType) { + + final String typeNameValue = HawkbitCommonUtil.trimAndNullIfEmpty(typeName.getValue()); + final String typeDescValue = HawkbitCommonUtil.trimAndNullIfEmpty(typeDesc.getValue()); + if (null != typeNameValue) { + existingType.setName(typeNameValue); + + existingType.setDescription(null != typeDescValue ? typeDescValue : null); + + existingType.setColour(getColorPickedSting()); + swTypeManagementService.updateSoftwareModuleType(existingType); + uiNotification.displaySuccess(i18n.get("message.update.success", new Object[] { existingType.getName() })); + closeWindow(); + eventBus.publish(this, + new SoftwareModuleTypeEvent(SoftwareModuleTypeEnum.UPDATE_SOFTWARE_MODULE_TYPE, existingType)); + + } else { + uiNotification.displayValidationError(i18n.get("message.tag.update.mandatory")); + } + + } + + /** + * Open color picker on click of preview button. Auto select the color based + * on target tag if already selected. + */ + private void previewButtonClicked() { + if (!tagPreviewBtnClicked) { + final String selectedOption = (String) createOptiongroup.getValue(); + if (null != selectedOption && selectedOption.equalsIgnoreCase(updateTypeStr)) { + if (null != typeNameComboBox.getValue()) { + + final SoftwareModuleType typeSelected = swTypeManagementService + .findSoftwareModuleTypeByName(typeNameComboBox.getValue().toString()); + if (null != typeSelected) { + selectedColor = typeSelected.getColour() != null ? rgbToColorConverter(typeSelected.getColour()) + : rgbToColorConverter(DEFAULT_COLOR); + + } + + } else { + selectedColor = rgbToColorConverter(DEFAULT_COLOR); + } + } + selPreview.setColor(selectedColor); + fieldLayout.addComponent(sliders); + mainLayout.addComponent(colorPickerLayout); + mainLayout.setComponentAlignment(colorPickerLayout, Alignment.BOTTOM_CENTER); + } + tagPreviewBtnClicked = !tagPreviewBtnClicked; + } + + /** + * Covert RGB code to {@Color}. + * + * @param value + * RGB vale + * @return Color + */ + protected Color rgbToColorConverter(final String value) { + if (value.startsWith("rgb")) { + final String[] colors = value.substring(value.indexOf('(') + 1, value.length() - 1).split(","); + final int red = Integer.parseInt(colors[0]); + final int green = Integer.parseInt(colors[1]); + final int blue = Integer.parseInt(colors[2]); + if (colors.length > 3) { + final int alpha = (int) (Double.parseDouble(colors[3]) * 255d); + return new Color(red, green, blue, alpha); + } else { + return new Color(red, green, blue); + } + } + return null; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.components.colorpicker.HasColorChangeListener# + * addColorChangeListener(com.vaadin + * .ui.components.colorpicker.ColorChangeListener) + */ + @Override + public void addColorChangeListener(final ColorChangeListener listener) { + LOG.debug("inside addColorChangeListener"); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.components.colorpicker.HasColorChangeListener# + * removeColorChangeListener(com. + * vaadin.ui.components.colorpicker.ColorChangeListener) + */ + @Override + public void removeColorChangeListener(final ColorChangeListener listener) { + LOG.debug("inside removeColorChangeListener"); + + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.components.colorpicker.ColorSelector#setColor(com.vaadin. + * shared.ui.colorpicker .Color) + */ + @Override + public void setColor(final Color color) { + if (color == null) { + return; + } + selectedColor = color; + selPreview.setColor(selectedColor); + final String colorPickedPreview = selPreview.getColor().getCSS(); + if (typeName.isEnabled() && null != colorSelect) { + createDynamicStyleForComponents(typeName, typeKey, typeDesc, colorPickedPreview); + colorSelect.setColor(selPreview.getColor()); + } + + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.components.colorpicker.ColorSelector#getColor() + */ + @Override + public Color getColor() { + return null; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.components.colorpicker.ColorChangeListener#colorChanged(com + * .vaadin.ui.components .colorpicker.ColorChangeEvent) + */ + @Override + public void colorChanged(final ColorChangeEvent event) { + setColor(event.getColor()); + for (final ColorSelector select : selectors) { + if (!event.getSource().equals(select) && select.equals(this) && !select.getColor().equals(selectedColor)) { + select.setColor(selectedColor); + } + } + setRgbSliderValues(selectedColor); + getPreviewButtonColor(event.getColor().getCSS()); + createDynamicStyleForComponents(typeName, typeKey, typeDesc, event.getColor().getCSS()); + } + + /** + * Set tag name and desc field border color based on chosen color. + * + * @param tagName + * @param tagDesc + * @param taregtTagColor + */ + private void createDynamicStyleForComponents(final TextField typeName, final TextField typeKey, + final TextArea typeDesc, final String typeTagColor) { + getTargetDynamicStyles(typeTagColor); + typeName.addStyleName(TYPE_NAME_DYNAMIC_STYLE); + typeKey.addStyleName(TYPE_NAME_DYNAMIC_STYLE); + typeDesc.addStyleName(TYPE_DESC_DYNAMIC_STYLE); + } + + /** + * Get target style - Dynamically as per the color picked, cannot be done + * from the static css. + * + * @param colorPickedPreview + */ + private void getTargetDynamicStyles(final String colorPickedPreview) { + Page.getCurrent().getJavaScript() + .execute(HawkbitCommonUtil.changeToNewSelectedPreviewColor(colorPickedPreview)); + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtype/SMTypeFilterButtonClick.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtype/SMTypeFilterButtonClick.java new file mode 100755 index 000000000..4a4006c2a --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtype/SMTypeFilterButtonClick.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.smtype; + +import java.io.Serializable; + +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.ui.artifacts.event.SMFilterEvent; +import org.eclipse.hawkbit.ui.artifacts.state.ArtifactUploadState; +import org.eclipse.hawkbit.ui.common.filterlayout.AbstractFilterSingleButtonClick; +import org.springframework.beans.factory.annotation.Autowired; +import org.vaadin.spring.events.EventBus; + +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; +import com.vaadin.ui.Button; + +/** + * Single button click behaviour of filter buttons layout. + * + * + * + */ +@SpringComponent +@ViewScope +public class SMTypeFilterButtonClick extends AbstractFilterSingleButtonClick implements Serializable { + + private static final long serialVersionUID = 3707945900524967887L; + + @Autowired + private transient EventBus.SessionEventBus eventBus; + + @Autowired + private ArtifactUploadState artifactUploadState; + + @Autowired + private transient SoftwareManagement softwareManagement; + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.layouts.SPFilterButtonClickBehaviour# + * filterUnClicked(com.vaadin.ui. Button) + */ + @Override + protected void filterUnClicked(final Button clickedButton) { + artifactUploadState.getSoftwareModuleFilters().setSoftwareModuleType(null); + eventBus.publish(this, SMFilterEvent.FILTER_BY_TYPE); + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.layouts.SPFilterButtonClickBehaviour#filterClicked + * (com.vaadin.ui.Button ) + */ + @Override + protected void filterClicked(final Button clickedButton) { + final SoftwareModuleType softwareModuleType = softwareManagement + .findSoftwareModuleTypeByName(clickedButton.getData().toString()); + artifactUploadState.getSoftwareModuleFilters().setSoftwareModuleType(softwareModuleType); + eventBus.publish(this, SMFilterEvent.FILTER_BY_TYPE); + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtype/SMTypeFilterButtons.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtype/SMTypeFilterButtons.java new file mode 100755 index 000000000..43c2452c2 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtype/SMTypeFilterButtons.java @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.smtype; + +import javax.annotation.PreDestroy; + +import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleTypeEvent; +import org.eclipse.hawkbit.ui.artifacts.event.UploadArtifactUIEvent; +import org.eclipse.hawkbit.ui.artifacts.event.UploadViewAcceptCriteria; +import org.eclipse.hawkbit.ui.artifacts.state.ArtifactUploadState; +import org.eclipse.hawkbit.ui.common.SoftwareModuleTypeBeanQuery; +import org.eclipse.hawkbit.ui.common.filterlayout.AbstractFilterButtonClickBehaviour; +import org.eclipse.hawkbit.ui.common.filterlayout.AbstractFilterButtons; +import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; +import org.eclipse.hawkbit.ui.utils.SPUIComponetIdProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.vaadin.addons.lazyquerycontainer.BeanQueryFactory; +import org.vaadin.addons.lazyquerycontainer.LazyQueryContainer; +import org.vaadin.spring.events.EventBus; +import org.vaadin.spring.events.EventScope; +import org.vaadin.spring.events.annotation.EventBusListenerMethod; + +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.DropHandler; +import com.vaadin.event.dd.acceptcriteria.AcceptCriterion; +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; + +/** + * Software module type filter buttons. + * + * + * + */ +@SpringComponent +@ViewScope +public class SMTypeFilterButtons extends AbstractFilterButtons { + + private static final long serialVersionUID = 169198312654380358L; + + @Autowired + private ArtifactUploadState artifactUploadState; + + @Autowired + private UploadViewAcceptCriteria uploadViewAcceptCriteria; + + @Autowired + private transient EventBus.SessionEventBus eventBus; + + /** + * Initialize component. + * + * @param filterButtonClickBehaviour + * the clickable behaviour. + */ + public void init(final AbstractFilterButtonClickBehaviour filterButtonClickBehaviour) { + super.init(filterButtonClickBehaviour); + eventBus.subscribe(this); + } + + @PreDestroy + void destroy() { + eventBus.unsubscribe(this); + } + + @EventBusListenerMethod(scope = EventScope.SESSION) + void onEvent(final SoftwareModuleTypeEvent event) { + if (event.getSoftwareModuleTypeEnum() == SoftwareModuleTypeEvent.SoftwareModuleTypeEnum.ADD_SOFTWARE_MODULE_TYPE + || event.getSoftwareModuleTypeEnum() == SoftwareModuleTypeEvent.SoftwareModuleTypeEnum.UPDATE_SOFTWARE_MODULE_TYPE + || event.getSoftwareModuleTypeEnum() == SoftwareModuleTypeEvent.SoftwareModuleTypeEnum.DELETE_SOFTWARE_MODULE_TYPE + && event.getSoftwareModuleType() != null) { + refreshTable(); + } + } + + @EventBusListenerMethod(scope = EventScope.SESSION) + void onEvent(final UploadArtifactUIEvent event) { + if (event == UploadArtifactUIEvent.DELETED_ALL_SOFWARE_TYPE) { + refreshTable(); + } + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.layouts.SPFilterButtons#getButtonsTableId() + */ + @Override + protected String getButtonsTableId() { + return SPUIComponetIdProvider.SW_MODULE_TYPE_TABLE_ID; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.layouts.SPFilterButtons# + * createButtonsLazyQueryContainer() + */ + @Override + protected LazyQueryContainer createButtonsLazyQueryContainer() { + return HawkbitCommonUtil.createLazyQueryContainer( + new BeanQueryFactory(SoftwareModuleTypeBeanQuery.class)); + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.layouts.SPFilterButtons#isClickedByDefault(java. + * lang.Long) + */ + @Override + protected boolean isClickedByDefault(final Long buttonId) { + return artifactUploadState.getSoftwareModuleFilters().getSoftwareModuleType().isPresent() && artifactUploadState + .getSoftwareModuleFilters().getSoftwareModuleType().get().getId().equals(buttonId); + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.layouts.SPFilterButtons#createButtonId(java.lang. + * String) + */ + @Override + protected String createButtonId(final String name) { + return SPUIComponetIdProvider.SM_TYPE_FILTER_BTN_ID + name; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.filterlayout.SPFilterButtons# + * getFilterButtonDropHandler() + */ + @Override + protected DropHandler getFilterButtonDropHandler() { + return new DropHandler() { + private static final long serialVersionUID = 1L; + + @Override + public AcceptCriterion getAcceptCriterion() { + return uploadViewAcceptCriteria; + } + + @Override + public void drop(final DragAndDropEvent event) { + /* Not required */ + } + }; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.filterlayout.AbstractFilterButtons# + * getButttonWrapperId() + */ + @Override + protected String getButttonWrapperIdPrefix() { + return SPUIComponetIdProvider.UPLOAD_TYPE_BUTTON_PREFIX; + } + + @Override + protected String getButtonWrapperData() { + return null; + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtype/SMTypeFilterHeader.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtype/SMTypeFilterHeader.java new file mode 100755 index 000000000..d0292dd48 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtype/SMTypeFilterHeader.java @@ -0,0 +1,153 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.smtype; + +import javax.annotation.PostConstruct; + +import org.eclipse.hawkbit.repository.SpPermissionChecker; +import org.eclipse.hawkbit.ui.artifacts.event.UploadArtifactUIEvent; +import org.eclipse.hawkbit.ui.artifacts.state.ArtifactUploadState; +import org.eclipse.hawkbit.ui.common.filterlayout.AbstractFilterHeader; +import org.eclipse.hawkbit.ui.utils.SPUIComponetIdProvider; +import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; +import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; +import org.springframework.beans.factory.annotation.Autowired; +import org.vaadin.spring.events.EventBus; + +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.UI; +import com.vaadin.ui.Window; + +/** + * Software module type filter buttons header. + * + * + * + */ +@SpringComponent +@ViewScope +public class SMTypeFilterHeader extends AbstractFilterHeader { + + private static final long serialVersionUID = -4855810338059032342L; + + @Autowired + private SpPermissionChecker permChecker; + + @Autowired + private transient EventBus.SessionEventBus eventbus; + + @Autowired + private ArtifactUploadState artifactUploadState; + + @Autowired + private CreateUpdateSoftwareTypeLayout createUpdateSWTypeLayout; + + /** + * Initialize the components. + */ + @PostConstruct + protected void init() { + super.init(); + if (permChecker.hasCreateDistributionPermission() || permChecker.hasUpdateDistributionPermission()) { + createUpdateSWTypeLayout.init(); + } + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.layouts.SPFilterHeader#hasCreateUpdatePermission() + */ + @Override + protected boolean hasCreateUpdatePermission() { + return permChecker.hasCreateDistributionPermission() || permChecker.hasUpdateDistributionPermission(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.layouts.SPFilterHeader#getTitle() + */ + @Override + protected String getTitle() { + return SPUILabelDefinitions.TYPE; + } + + /* + * (non-Javadoc) + * + * @see hawkbit.server.ui.layouts.SPFilterHeader#settingsIconClicked(com. + * vaadin.ui.Button.ClickEvent ) + */ + @Override + protected void settingsIconClicked(final ClickEvent event) { + final Window addUpdateWindow = createUpdateSWTypeLayout.getWindow(); + UI.getCurrent().addWindow(addUpdateWindow); + addUpdateWindow.setVisible(Boolean.TRUE); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.layouts.SPFilterHeader#dropHitsRequired() + */ + @Override + protected boolean dropHitsRequired() { + return false; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.filterlayout.SPFilterHeader# + * hideFilterButtonLayout() + */ + @Override + protected void hideFilterButtonLayout() { + artifactUploadState.setSwTypeFilterClosed(true); + eventbus.publish(this, UploadArtifactUIEvent.HIDE_FILTER_BY_TYPE); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.filterlayout.SPFilterHeader# + * getConfigureFilterButtonId() + */ + @Override + protected String getConfigureFilterButtonId() { + return SPUIDefinitions.ADD_SOFTWARE_MODULE_TYPE; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.hawkbit.server.ui.common.filterlayout.AbstractFilterHeader# + * getHideButtonId() + */ + @Override + protected String getHideButtonId() { + return SPUIComponetIdProvider.SM_SHOW_FILTER_BUTTON_ID; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.ui.common.filterlayout.AbstractFilterHeader# + * isAddTagRequired() + */ + @Override + protected boolean isAddTagRequired() { + return true; + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtype/SMTypeFilterLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtype/SMTypeFilterLayout.java new file mode 100755 index 000000000..6bd57d945 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtype/SMTypeFilterLayout.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.smtype; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.eclipse.hawkbit.ui.artifacts.event.UploadArtifactUIEvent; +import org.eclipse.hawkbit.ui.artifacts.state.ArtifactUploadState; +import org.eclipse.hawkbit.ui.common.filterlayout.AbstractFilterLayout; +import org.springframework.beans.factory.annotation.Autowired; +import org.vaadin.spring.events.EventBus; +import org.vaadin.spring.events.EventScope; +import org.vaadin.spring.events.annotation.EventBusListenerMethod; + +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; + +/** + * Software module type filter buttons layout. + * + * + * + */ +@SpringComponent +@ViewScope +public class SMTypeFilterLayout extends AbstractFilterLayout { + + private static final long serialVersionUID = 1581066345157393665L; + + @Autowired + private transient EventBus.SessionEventBus eventbus; + + @Autowired + private SMTypeFilterHeader smTypeFilterHeader; + + @Autowired + private SMTypeFilterButtons smTypeFilterButtons; + + @Autowired + private SMTypeFilterButtonClick smTypeFilterButtonClick; + + @Autowired + private ArtifactUploadState artifactUploadState; + + /** + * Initialize the filter layout. + */ + @PostConstruct + void init() { + super.init(smTypeFilterHeader, smTypeFilterButtons, smTypeFilterButtonClick); + eventbus.subscribe(this); + } + + @EventBusListenerMethod(scope = EventScope.SESSION) + void onEvent(final UploadArtifactUIEvent event) { + if (event == UploadArtifactUIEvent.HIDE_FILTER_BY_TYPE) { + setVisible(false); + } + if (event == UploadArtifactUIEvent.SHOW_FILTER_BY_TYPE) { + setVisible(true); + } + } + + @PreDestroy + void destroy() { + /* + * It's good manners to do this, even though vaadin-spring will + * automatically unsubscribe when this UI is garbage collected. + */ + eventbus.unsubscribe(this); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.hawkbit.server.ui.common.filterlayout.SPFilterLayout# + * onLoadIsTypeFilterIsClosed() + */ + @Override + public Boolean onLoadIsTypeFilterIsClosed() { + return artifactUploadState.isSwTypeFilterClosed(); + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/state/ArtifactUploadState.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/state/ArtifactUploadState.java new file mode 100755 index 000000000..d653bb371 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/state/ArtifactUploadState.java @@ -0,0 +1,201 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.state; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.springframework.beans.factory.annotation.Autowired; + +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.VaadinSessionScope; + +/** + * User status of Artifact upload. + * + * + */ +@VaadinSessionScope +@SpringComponent +public class ArtifactUploadState implements Serializable { + + private static final long serialVersionUID = 8273440375917450859L; + + @Autowired + private SoftwareModuleFilters softwareModuleFilters; + + private final Map deleteSofwareModules = new HashMap<>(); + + private final Set fileSelected = new HashSet(); + + private Long selectedBaseSwModuleId; + + private SoftwareModule selectedBaseSoftwareModule; + + private final Map baseSwModuleList = new HashMap(); + + private Set selectedSoftwareModules = Collections.emptySet(); + + private boolean swTypeFilterClosed = Boolean.FALSE; + + private boolean isSwModuleTableMaximized = Boolean.FALSE; + + private boolean isArtifactDetailsMaximized = Boolean.FALSE; + + private final Set selectedDeleteSWModuleTypes = new HashSet<>(); + + private boolean noDataAvilableSoftwareModule = Boolean.FALSE; + + /** + * Set software. + * + * @return + */ + public SoftwareModuleFilters getSoftwareModuleFilters() { + return softwareModuleFilters; + } + + /** + * @return the selectedSofwareModules + */ + public Map getDeleteSofwareModules() { + return deleteSofwareModules; + } + + /** + * @return the fileSelected + */ + public Set getFileSelected() { + return fileSelected; + } + + /** + * @return the selectedBaseSwModuleId + */ + public Optional getSelectedBaseSwModuleId() { + return this.selectedBaseSwModuleId != null ? Optional.of(this.selectedBaseSwModuleId) : Optional.empty(); + } + + /** + * @param selectedBaseSwModuleId + * the selectedBaseSwModuleId to set + */ + public void setSelectedBaseSwModuleId(final Long selectedBaseSwModuleId) { + this.selectedBaseSwModuleId = selectedBaseSwModuleId; + } + + /** + * @return the selectedBaseSoftwareModule + */ + public Optional getSelectedBaseSoftwareModule() { + return this.selectedBaseSoftwareModule == null ? Optional.empty() + : Optional.of(this.selectedBaseSoftwareModule); + } + + /** + * @param selectedBaseSoftwareModule + * the selectedBaseSoftwareModule to set + */ + public void setSelectedBaseSoftwareModule(final SoftwareModule selectedBaseSoftwareModule) { + this.selectedBaseSoftwareModule = selectedBaseSoftwareModule; + } + + /** + * @return the baseSwModuleList + */ + public Map getBaseSwModuleList() { + return baseSwModuleList; + } + + /** + * @return the selectedSoftwareModules + */ + public Set getSelectedSoftwareModules() { + return selectedSoftwareModules; + } + + /** + * @param selectedSoftwareModules + * the selectedSoftwareModules to set + */ + public void setSelectedSoftwareModules(final Set selectedSoftwareModules) { + this.selectedSoftwareModules = selectedSoftwareModules; + } + + /** + * @return the swTypeFilterClosed + */ + public boolean isSwTypeFilterClosed() { + return swTypeFilterClosed; + } + + /** + * @param swTypeFilterClosed + * the swTypeFilterClosed to set + */ + public void setSwTypeFilterClosed(final boolean swTypeFilterClosed) { + this.swTypeFilterClosed = swTypeFilterClosed; + } + + /** + * @return the isSwModuleTableMaximized + */ + public boolean isSwModuleTableMaximized() { + return isSwModuleTableMaximized; + } + + /** + * @param isSwModuleTableMaximized + * the isSwModuleTableMaximized to set + */ + public void setSwModuleTableMaximized(final boolean isSwModuleTableMaximized) { + this.isSwModuleTableMaximized = isSwModuleTableMaximized; + } + + public Set getSelectedDeleteSWModuleTypes() { + return selectedDeleteSWModuleTypes; + } + + /** + * @return the isArtifactDetailsMaximized + */ + public boolean isArtifactDetailsMaximized() { + return isArtifactDetailsMaximized; + } + + /** + * @param isArtifactDetailsMaximized + * the isArtifactDetailsMaximized to set + */ + public void setArtifactDetailsMaximized(final boolean isArtifactDetailsMaximized) { + this.isArtifactDetailsMaximized = isArtifactDetailsMaximized; + } + + /** + * @return the noDataAvilableSoftwareModule + */ + public boolean isNoDataAvilableSoftwareModule() { + return noDataAvilableSoftwareModule; + } + + /** + * @param noDataAvilableSoftwareModule + * the noDataAvilableSoftwareModule to set + */ + public void setNoDataAvilableSoftwareModule(final boolean noDataAvilableSoftwareModule) { + this.noDataAvilableSoftwareModule = noDataAvilableSoftwareModule; + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/state/CustomFile.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/state/CustomFile.java new file mode 100755 index 000000000..9b29c780a --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/state/CustomFile.java @@ -0,0 +1,203 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.state; + +import java.io.Serializable; + +import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; + +/** + * Custom file to hold details of uploaded file. + * + * + * + */ +public class CustomFile implements Serializable { + + private static final long serialVersionUID = -5902321650745311767L; + + private final String fileName; + + private long fileSize; + + private String filePath; + + private String baseSoftwareModuleName; + + private String baseSoftwareModuleVersion; + + private String mimeType; + + /** + * Used to specify if the file is uploaded successfully. + */ + private Boolean isValid = Boolean.TRUE; + + /** + * Reason if upload fails. + */ + private String failureReason; + + /** + * Initialize details. + * + * @param fileName + * uploaded file name + * @param fileSize + * uploaded file size + * @param filePath + * uploaded file path + * @param baseSoftwareModuleName + * software module name + * @param baseSoftwareModuleVersion + * software module version + * @param mimeType + * the mimeType of the file + */ + public CustomFile(final String fileName, final long fileSize, final String filePath, + final String baseSoftwareModuleName, final String baseSoftwareModuleVersion, final String mimeType) { + this.fileName = fileName; + this.fileSize = fileSize; + this.filePath = filePath; + this.baseSoftwareModuleName = baseSoftwareModuleName; + this.baseSoftwareModuleVersion = baseSoftwareModuleVersion; + this.mimeType = mimeType; + } + + /** + * Initialize details. + * + * @param fileName + * uploaded file name + * @param baseSoftwareModuleName + * software module name + * @param baseSoftwareModuleVersion + * software module version + */ + public CustomFile(final String fileName, final String baseSoftwareModuleName, + final String baseSoftwareModuleVersion) { + this.fileName = fileName; + this.baseSoftwareModuleName = baseSoftwareModuleName; + this.baseSoftwareModuleVersion = baseSoftwareModuleVersion; + } + + /** + * Default constructor with the immutable fileName set. + * + * @param fileName + * uploaded file name + */ + public CustomFile(final String fileName) { + this.fileName = fileName; + } + + public String getBaseSoftwareModuleName() { + return baseSoftwareModuleName; + } + + public void setBaseSoftwareModuleName(final String baseSoftwareModuleName) { + this.baseSoftwareModuleName = baseSoftwareModuleName; + } + + public String getBaseSoftwareModuleVersion() { + return baseSoftwareModuleVersion; + } + + public void setBaseSoftwareModuleVersion(final String baseSoftwareModuleVersion) { + this.baseSoftwareModuleVersion = baseSoftwareModuleVersion; + } + + public String getFileName() { + return fileName; + } + + public long getFileSize() { + return fileSize; + } + + public void setFileSize(final long fileSize) { + this.fileSize = fileSize; + } + + public String getFilePath() { + return filePath; + } + + public void setFilePath(final String filePath) { + this.filePath = filePath; + } + + public String getMimeType() { + return mimeType; + } + + /** + * + * @return the isValid + */ + public Boolean getIsValid() { + return isValid; + } + + /** + * @param isValid + * the isValid to set + */ + public void setIsValid(final Boolean isValid) { + this.isValid = isValid; + } + + /** + * @param mimeType + * the mimeType to set + */ + public void setMimeType(final String mimeType) { + this.mimeType = mimeType; + } + + /** + * @return the failureReason + */ + public String getFailureReason() { + return failureReason; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (fileName == null ? 0 : fileName.hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof CustomFile)) { + return false; + } + final CustomFile other = (CustomFile) obj; + return HawkbitCommonUtil.bothSame(fileName, other.fileName) + && HawkbitCommonUtil.bothSame(baseSoftwareModuleName, other.baseSoftwareModuleName) + && HawkbitCommonUtil.bothSame(baseSoftwareModuleVersion, other.baseSoftwareModuleVersion); + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/state/SoftwareModuleFilters.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/state/SoftwareModuleFilters.java new file mode 100755 index 000000000..7f0049ae2 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/state/SoftwareModuleFilters.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.state; + +import java.io.Serializable; +import java.util.Optional; + +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; + +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.VaadinSessionScope; + +/** + * Softwrae module filters. + * + * + */ +@VaadinSessionScope +@SpringComponent +public class SoftwareModuleFilters implements Serializable { + + private static final long serialVersionUID = -5251492630546463593L; + + private SoftwareModuleType softwareModuleType; + + private String searchText; + + public Optional getSoftwareModuleType() { + return softwareModuleType == null ? Optional.empty() : Optional.of(softwareModuleType); + } + + public void setSoftwareModuleType(final SoftwareModuleType softwareModuleType) { + this.softwareModuleType = softwareModuleType; + } + + public Optional getSearchText() { + return searchText == null ? Optional.empty() : Optional.of(searchText); + } + + public void setSearchText(final String searchText) { + this.searchText = searchText; + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadConfirmationwindow.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadConfirmationwindow.java new file mode 100755 index 000000000..51ac43ba5 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadConfirmationwindow.java @@ -0,0 +1,705 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.upload; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.hawkbit.repository.ArtifactManagement; +import org.eclipse.hawkbit.repository.exception.ArtifactUploadFailedException; +import org.eclipse.hawkbit.repository.exception.InvalidMD5HashException; +import org.eclipse.hawkbit.repository.exception.InvalidSHA1HashException; +import org.eclipse.hawkbit.repository.model.LocalArtifact; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.ui.artifacts.state.ArtifactUploadState; +import org.eclipse.hawkbit.ui.artifacts.state.CustomFile; +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleSmallNoBorder; +import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleTiny; +import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPUIComponetIdProvider; +import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; +import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; +import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; +import org.eclipse.hawkbit.ui.utils.SpringContextHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.data.Item; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.event.FieldEvents.TextChangeEvent; +import com.vaadin.server.FontAwesome; +import com.vaadin.server.Page; +import com.vaadin.shared.ui.label.ContentMode; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.Button; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.Table; +import com.vaadin.ui.TextField; +import com.vaadin.ui.UI; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; +import com.vaadin.ui.themes.ValoTheme; + +/** + * Artifact upload confirmation popup. + * + * + * + * + * + */ +public class UploadConfirmationwindow implements Button.ClickListener { + + private static final long serialVersionUID = -1679035890140031740L; + + private static final Logger LOG = LoggerFactory.getLogger(UploadConfirmationwindow.class); + + private static final String MD5_CHECKSUM = "md5Checksum"; + + private static final String SHA1_CHECKSUM = "sha1Checksum"; + + private static final String FILE_NAME = "fileName"; + + private static final String SW_MODULE_NAME = "swModuleName"; + + private static final String SIZE = "size"; + + private static final String ACTION = "action"; + + private static final String BASE_SOFTWARE_ID = "softwareModuleId"; + + private static final String FILE_NAME_LAYOUT = "fileNameLayout"; + + private static final String WARNING_ICON = "warningIcon"; + + private static final String CUSTOM_FILE = "customFile"; + + private static final String ARTIFACT_UPLOAD_EXCEPTION = "Artifact upload exception:"; + + private static final String ALREADY_EXISTS_MSG = "upload.artifact.alreadyExists"; + + private final I18N i18n; + + private Window uploadConfrimationWindow; + + private Button uploadBtn; + + private Button cancelBtn; + + private Table uploadDetailsTable; + + private final UploadLayout uploadLayout; + + private IndexedContainer tabelContainer; + + private final List uploadResultList = new ArrayList(); + + private VerticalLayout uploadArtifactDetails; + + private UploadResultWindow currentUploadResultWindow; + + private int redErrorLabelCount = 0; + + private final ArtifactUploadState artifactUploadState; + + /** + * Initialize the upload confirmation window. + * + * @param artifactUploadView + * reference of upload layout. + * @param artifactUploadState + * reference of session variable {@link ArtifactUploadState}. + */ + public UploadConfirmationwindow(final UploadLayout artifactUploadView, + final ArtifactUploadState artifactUploadState) { + this.uploadLayout = artifactUploadView; + this.artifactUploadState = artifactUploadState; + i18n = artifactUploadView.getI18n(); + createRequiredComponents(); + buildLayout(); + } + + private Boolean checkIfArtifactDetailsDispalyed(final Long bSoftwareModuleId) { + if (artifactUploadState.getSelectedBaseSoftwareModule().isPresent() + && artifactUploadState.getSelectedBaseSoftwareModule().get().getId().equals(bSoftwareModuleId)) { + return true; + } + return false; + } + + private Boolean preUploadValidation(final List itemIds) { + Boolean validationSuccess = true; + for (final String itemId : itemIds) { + final Item item = tabelContainer.getItem(itemId); + final String providedFileName = (String) item.getItemProperty(FILE_NAME).getValue(); + if (HawkbitCommonUtil.trimAndNullIfEmpty(providedFileName) == null) { + validationSuccess = false; + break; + } + } + return validationSuccess; + } + + private void createRequiredComponents() { + uploadBtn = SPUIComponentProvider.getButton(SPUIComponetIdProvider.UPLOAD_BUTTON, SPUILabelDefinitions.SUBMIT, + SPUILabelDefinitions.SUBMIT, ValoTheme.BUTTON_PRIMARY, false, null, SPUIButtonStyleTiny.class); + uploadBtn.addClickListener(this); + cancelBtn = SPUIComponentProvider.getButton(SPUIComponetIdProvider.UPLOAD_DISCARD_DETAILS_BUTTON, + SPUILabelDefinitions.DISCARD, SPUILabelDefinitions.DISCARD, null, false, null, + SPUIButtonStyleTiny.class); + cancelBtn.addClickListener(this); + + uploadDetailsTable = new Table(); + uploadDetailsTable.addStyleName("artifact-table"); + uploadDetailsTable.setSizeFull(); + uploadDetailsTable.setId(SPUIComponetIdProvider.UPLOAD_ARTIFACT_DETAILS_TABLE); + uploadDetailsTable.addStyleName(ValoTheme.TABLE_BORDERLESS); + uploadDetailsTable.addStyleName(ValoTheme.TABLE_NO_VERTICAL_LINES); + uploadDetailsTable.addStyleName(ValoTheme.TABLE_SMALL); + + setTableContainer(); + populateUploadDetailsTable(); + } + + /** + * Warning icon is displayed, if an artifact exists with same provided file + * name. Error icon is displayed ,if file name entered is duplicate. + * + * @param warningIconLabel + * warning/error label + * @param fileName + * provided file name + * @param itemId + * item id of the current row + */ + private void setWarningIcon(final Label warningIconLabel, final String fileName, final Object itemId) { + final Item item = uploadDetailsTable.getItem(itemId); + final ArtifactManagement artifactManagement = SpringContextHelper.getBean(ArtifactManagement.class); + if (HawkbitCommonUtil.trimAndNullIfEmpty(fileName) != null) { + final Long baseSwId = (Long) item.getItemProperty(BASE_SOFTWARE_ID).getValue(); + final List artifactList = artifactManagement.findByFilenameAndSoftwareModule(fileName, + baseSwId); + if (!artifactList.isEmpty()) { + warningIconLabel.setVisible(true); + if (isErrorIcon(warningIconLabel)) { + warningIconLabel.removeStyleName(SPUIStyleDefinitions.ERROR_LABEL); + redErrorLabelCount--; + } + warningIconLabel.setDescription(i18n.get(ALREADY_EXISTS_MSG)); + if (checkForDuplicate(fileName, itemId, baseSwId)) { + warningIconLabel.setDescription(i18n.get("message.duplicate.filename")); + warningIconLabel.addStyleName(SPUIStyleDefinitions.ERROR_LABEL); + redErrorLabelCount++; + } + } else { + warningIconLabel.setVisible(false); + if (warningIconLabel.getStyleName().contains(SPUIStyleDefinitions.ERROR_LABEL)) { + warningIconLabel.removeStyleName(SPUIStyleDefinitions.ERROR_LABEL); + warningIconLabel.setDescription(i18n.get(ALREADY_EXISTS_MSG)); + redErrorLabelCount--; + } + } + } + } + + private Boolean checkForDuplicate(final String fileName, final Object itemId, final Long currentBaseSwId) { + for (final Object newItemId : tabelContainer.getItemIds()) { + final Item newItem = tabelContainer.getItem(newItemId); + final Long newBaseSwId = (Long) newItem.getItemProperty(BASE_SOFTWARE_ID).getValue(); + final String newFileName = (String) newItem.getItemProperty(FILE_NAME).getValue(); + if (!newItemId.equals(itemId) && newBaseSwId.equals(currentBaseSwId) && newFileName.equals(fileName)) { + return true; + } + } + return false; + } + + private void populateUploadDetailsTable() { + for (final CustomFile customFile : uploadLayout.getFileSelected()) { + final String swNameVersion = HawkbitCommonUtil.getFormattedNameVersion( + customFile.getBaseSoftwareModuleName(), customFile.getBaseSoftwareModuleVersion()); + final String itemId = swNameVersion + "/" + customFile.getFileName(); + final Item newItem = tabelContainer.addItem(itemId); + final SoftwareModule bSoftwareModule = artifactUploadState.getBaseSwModuleList().get(swNameVersion); + newItem.getItemProperty(BASE_SOFTWARE_ID).setValue(bSoftwareModule.getId()); + + addFileNameLayout(newItem, swNameVersion, customFile.getFileName(), itemId); + + newItem.getItemProperty(SW_MODULE_NAME).setValue(HawkbitCommonUtil.getFormatedLabel(swNameVersion)); + newItem.getItemProperty(SIZE).setValue(customFile.getFileSize()); + final Button deleteIcon = SPUIComponentProvider.getButton( + SPUIComponetIdProvider.UPLOAD_DELETE_ICON + "-" + itemId, "", SPUILabelDefinitions.DISCARD, + ValoTheme.BUTTON_TINY + " " + "redicon", true, FontAwesome.TRASH_O, + SPUIButtonStyleSmallNoBorder.class); + deleteIcon.addClickListener(this); + deleteIcon.setData(itemId); + newItem.getItemProperty(ACTION).setValue(deleteIcon); + + final TextField sha1 = SPUIComponentProvider.getTextField("", ValoTheme.TEXTFIELD_TINY, false, null, null, + true, SPUILabelDefinitions.TEXT_FIELD_MAX_LENGTH); + sha1.setId(swNameVersion + "/" + customFile.getFileName() + "/sha1"); + + final TextField md5 = SPUIComponentProvider.getTextField("", ValoTheme.TEXTFIELD_TINY, false, null, null, + true, SPUILabelDefinitions.TEXT_FIELD_MAX_LENGTH); + md5.setId(swNameVersion + "/" + customFile.getFileName() + "/md5"); + + final TextField customFileName = SPUIComponentProvider.getTextField("", ValoTheme.TEXTFIELD_TINY, false, + null, null, true, SPUILabelDefinitions.TEXT_FIELD_MAX_LENGTH); + customFileName.setId(swNameVersion + "/" + customFile.getFileName() + "/customFileName"); + newItem.getItemProperty(SHA1_CHECKSUM).setValue(sha1); + newItem.getItemProperty(MD5_CHECKSUM).setValue(md5); + newItem.getItemProperty(CUSTOM_FILE).setValue(customFile); + } + } + + private void addFileNameLayout(final Item newItem, final String baseSoftwareModuleNameVersion, + final String customFileName, final String itemId) { + final HorizontalLayout horizontalLayout = new HorizontalLayout(); + final TextField fileNameTextField = SPUIComponentProvider.getTextField("", ValoTheme.TEXTFIELD_TINY, false, + null, null, true, SPUILabelDefinitions.TEXT_FIELD_MAX_LENGTH); + fileNameTextField.setId(baseSoftwareModuleNameVersion + "/" + customFileName + "/customFileName"); + fileNameTextField.setData(baseSoftwareModuleNameVersion + "/" + customFileName); + fileNameTextField.setValue(customFileName); + + newItem.getItemProperty(FILE_NAME).setValue(fileNameTextField.getValue()); + + final Label warningIconLabel = getWarningLabel(); + warningIconLabel.setId(baseSoftwareModuleNameVersion + "/" + customFileName + "/icon"); + setWarningIcon(warningIconLabel, fileNameTextField.getValue(), itemId); + newItem.getItemProperty(WARNING_ICON).setValue(warningIconLabel); + + horizontalLayout.addComponent(fileNameTextField); + horizontalLayout.setComponentAlignment(fileNameTextField, Alignment.MIDDLE_LEFT); + horizontalLayout.addComponent(warningIconLabel); + horizontalLayout.setComponentAlignment(warningIconLabel, Alignment.MIDDLE_RIGHT); + newItem.getItemProperty(FILE_NAME_LAYOUT).setValue(horizontalLayout); + + fileNameTextField.addTextChangeListener(event -> onFileNameChange(event, warningIconLabel, newItem)); + } + + private void onFileNameChange(final TextChangeEvent event, final Label warningIconLabel, final Item newItem) { + + final String itemId = (String) ((TextField) event.getComponent()).getData(); + final String fileName = event.getText(); + + final Boolean isWarningIconDisplayed = isWarningIcon(warningIconLabel); + setWarningIcon(warningIconLabel, fileName, itemId); + + final Long currentSwId = (Long) newItem.getItemProperty(BASE_SOFTWARE_ID).getValue(); + final String oldFileName = (String) newItem.getItemProperty(FILE_NAME).getValue(); + newItem.getItemProperty(FILE_NAME).setValue(event.getText()); + + // if warning was displayed prior and not displayed currently + if (isWarningIconDisplayed && !warningIconLabel.isVisible()) { + modifyIconOfSameSwId(itemId, currentSwId, oldFileName); + } + checkDuplicateEntry(itemId, currentSwId, event.getText(), oldFileName); + enableOrDisableUploadBtn(); + } + + private void enableOrDisableUploadBtn() { + if (redErrorLabelCount == 0) { + uploadBtn.setEnabled(true); + } else { + uploadBtn.setEnabled(false); + } + } + + /** + * If warning was displayed prior and not displayed currently ,the update + * other warning labels accordingly. + * + * @param itemId + * id of row which is deleted/whose file name modified. + * @param oldSwId + * software module id + * @param oldFileName + * file name before modification + */ + private void modifyIconOfSameSwId(final Object itemId, final Long oldSwId, final String oldFileName) { + for (final Object rowId : tabelContainer.getItemIds()) { + final Item newItem = tabelContainer.getItem(rowId); + final Long newBaseSwId = (Long) newItem.getItemProperty(BASE_SOFTWARE_ID).getValue(); + final String newFileName = (String) newItem.getItemProperty(FILE_NAME).getValue(); + if (!rowId.equals(itemId) && newBaseSwId.equals(oldSwId) && newFileName.equals(oldFileName)) { + final HorizontalLayout layout = (HorizontalLayout) newItem.getItemProperty(FILE_NAME_LAYOUT).getValue(); + final Label warningLabel = (Label) layout.getComponent(1); + if (warningLabel.isVisible()) { + warningLabel.removeStyleName(SPUIStyleDefinitions.ERROR_LABEL); + warningLabel.setDescription(i18n.get(ALREADY_EXISTS_MSG)); + newItem.getItemProperty(WARNING_ICON).setValue(warningLabel); + redErrorLabelCount--; + break; + } + } + } + + } + + /** + * Check if icon is warning icon and visible. + * + * @param icon + * label + * @return Boolean + */ + private Boolean isWarningIcon(final Label icon) { + if (icon.isVisible() && !icon.getStyleName().contains(SPUIStyleDefinitions.ERROR_LABEL)) { + return true; + } + return false; + } + + /** + * Check if icon is error icon and visible. + * + * @param icon + * label + * @return Boolean + */ + private Boolean isErrorIcon(final Label icon) { + if (icon.isVisible() && icon.getStyleName().contains(SPUIStyleDefinitions.ERROR_LABEL)) { + return true; + } + return false; + } + + private Label getWarningLabel() { + final Label warningIconLabel = new Label(); + warningIconLabel.addStyleName(ValoTheme.LABEL_SMALL); + warningIconLabel.setHeightUndefined(); + warningIconLabel.setContentMode(ContentMode.HTML); + warningIconLabel.setValue(FontAwesome.WARNING.getHtml()); + warningIconLabel.addStyleName("warningLabel"); + warningIconLabel.setVisible(false); + return warningIconLabel; + } + + private void newFileNameIsDuplicate(final Object itemId, final Long currentSwId, final String currentChangedText) { + for (final Object rowId : tabelContainer.getItemIds()) { + final Item currentItem = tabelContainer.getItem(itemId); + final Item newItem = tabelContainer.getItem(rowId); + final Long newBaseSwId = (Long) newItem.getItemProperty(BASE_SOFTWARE_ID).getValue(); + final String fileName = (String) newItem.getItemProperty(FILE_NAME).getValue(); + if (!rowId.equals(itemId) && newBaseSwId.equals(currentSwId) && fileName.equals(currentChangedText)) { + final HorizontalLayout layout = (HorizontalLayout) currentItem.getItemProperty(FILE_NAME_LAYOUT) + .getValue(); + final Label iconLabel = (Label) layout.getComponent(1); + if (!iconLabel.getStyleName().contains(SPUIStyleDefinitions.ERROR_LABEL)) { + iconLabel.setVisible(true); + iconLabel.setDescription(i18n.get("message.duplicate.filename")); + iconLabel.addStyleName(SPUIStyleDefinitions.ERROR_LABEL); + redErrorLabelCount++; + } + break; + } + } + } + + private void reValidateOtherFileNamesOfSameBaseSw(final Object itemId, final Long currentSwId, + final String oldFileName) { + Label warningLabel = null; + Label errorLabel = null; + int errorLabelCount = 0; + int duplicateCount = 0; + for (final Object rowId : tabelContainer.getItemIds()) { + final Item newItem = tabelContainer.getItem(rowId); + final Long newBaseSwId = (Long) newItem.getItemProperty(BASE_SOFTWARE_ID).getValue(); + final String newFileName = (String) newItem.getItemProperty(FILE_NAME).getValue(); + if (!rowId.equals(itemId) && newBaseSwId.equals(currentSwId) && newFileName.equals(oldFileName)) { + final HorizontalLayout layout = (HorizontalLayout) newItem.getItemProperty(FILE_NAME_LAYOUT).getValue(); + final Label icon = (Label) layout.getComponent(1); + duplicateCount++; + if (icon.isVisible()) { + if (!icon.getStyleName().contains(SPUIStyleDefinitions.ERROR_LABEL)) { + warningLabel = icon; + break; + } + errorLabel = icon; + errorLabelCount++; + } + + } + } + hideErrorIcon(warningLabel, errorLabelCount, duplicateCount, errorLabel, oldFileName, currentSwId); + } + + private void hideErrorIcon(final Label warningLabel, final int errorLabelCount, final int duplicateCount, + final Label errorLabel, final String oldFileName, final Long currentSwId) { + if (warningLabel == null && (errorLabelCount > 1 || duplicateCount == 1 && errorLabelCount == 1)) { + final ArtifactManagement artifactManagement = SpringContextHelper.getBean(ArtifactManagement.class); + final List artifactList = artifactManagement.findByFilenameAndSoftwareModule(oldFileName, + currentSwId); + errorLabel.removeStyleName(SPUIStyleDefinitions.ERROR_LABEL); + errorLabel.setDescription(i18n.get(ALREADY_EXISTS_MSG)); + if (artifactList.isEmpty()) { + errorLabel.setVisible(false); + } + redErrorLabelCount--; + } + } + + private void checkDuplicateEntry(final Object itemId, final Long currentSwId, final String newChangedText, + final String oldFileName) { + /** + * Check if newly entered file name is a duplicate. + */ + newFileNameIsDuplicate(itemId, currentSwId, newChangedText); + /** + * After the current changed file name is validated ,other files of same + * software module as has be revalidated. And icons are updated + * accordingly. + */ + reValidateOtherFileNamesOfSameBaseSw(itemId, currentSwId, oldFileName); + + } + + private void setTableContainer() { + tabelContainer = new IndexedContainer(); + tabelContainer.addContainerProperty(FILE_NAME_LAYOUT, HorizontalLayout.class, null); + tabelContainer.addContainerProperty(SW_MODULE_NAME, Label.class, null); + tabelContainer.addContainerProperty(SHA1_CHECKSUM, TextField.class, null); + tabelContainer.addContainerProperty(MD5_CHECKSUM, TextField.class, null); + tabelContainer.addContainerProperty(SIZE, Long.class, null); + tabelContainer.addContainerProperty(ACTION, Button.class, ""); + tabelContainer.addContainerProperty(FILE_NAME, String.class, null); + tabelContainer.addContainerProperty(BASE_SOFTWARE_ID, Long.class, null); + tabelContainer.addContainerProperty(WARNING_ICON, Label.class, null); + tabelContainer.addContainerProperty(CUSTOM_FILE, CustomFile.class, null); + + uploadDetailsTable.setContainerDataSource(tabelContainer); + uploadDetailsTable.setPageLength(10); + uploadDetailsTable.setColumnHeader(FILE_NAME_LAYOUT, i18n.get("upload.file.name")); + uploadDetailsTable.setColumnHeader(SW_MODULE_NAME, i18n.get("upload.swModuleTable.header")); + uploadDetailsTable.setColumnHeader(SHA1_CHECKSUM, i18n.get("upload.sha1")); + uploadDetailsTable.setColumnHeader(MD5_CHECKSUM, i18n.get("upload.md5")); + uploadDetailsTable.setColumnHeader(SIZE, i18n.get("upload.size")); + uploadDetailsTable.setColumnHeader(ACTION, i18n.get("upload.action")); + + uploadDetailsTable.setColumnExpandRatio(FILE_NAME_LAYOUT, 0.25f); + uploadDetailsTable.setColumnExpandRatio(SW_MODULE_NAME, 0.17f); + uploadDetailsTable.setColumnExpandRatio(SHA1_CHECKSUM, 0.2f); + uploadDetailsTable.setColumnExpandRatio(MD5_CHECKSUM, 0.2f); + uploadDetailsTable.setColumnExpandRatio(SIZE, 0.12f); + uploadDetailsTable.setColumnExpandRatio(ACTION, 0.06f); + + final Object[] visibileColumn = { FILE_NAME_LAYOUT, SW_MODULE_NAME, SHA1_CHECKSUM, MD5_CHECKSUM, SIZE, ACTION }; + uploadDetailsTable.setVisibleColumns(visibileColumn); + } + + private void buildLayout() { + final HorizontalLayout footer = getFooterLayout(); + + uploadArtifactDetails = new VerticalLayout(); + uploadArtifactDetails.setWidth(SPUIDefinitions.MIN_UPLOAD_CONFIRMATION_POPUP_WIDTH + "px"); + uploadArtifactDetails.addStyleName("confirmation-popup"); + uploadArtifactDetails.addComponent(uploadDetailsTable); + uploadArtifactDetails.setComponentAlignment(uploadDetailsTable, Alignment.MIDDLE_CENTER); + uploadArtifactDetails.addComponent(footer); + uploadArtifactDetails.setComponentAlignment(footer, Alignment.MIDDLE_CENTER); + + uploadConfrimationWindow = new Window(); + uploadConfrimationWindow.setContent(uploadArtifactDetails); + uploadConfrimationWindow.setResizable(Boolean.FALSE); + uploadConfrimationWindow.setClosable(Boolean.TRUE); + uploadConfrimationWindow.setDraggable(Boolean.TRUE); + uploadConfrimationWindow.setModal(true); + uploadConfrimationWindow.addCloseListener(event -> onPopupClose()); + uploadConfrimationWindow.setCaption(i18n.get("header.caption.upload.details")); + uploadConfrimationWindow.addStyleName(SPUIStyleDefinitions.CONFIRMATION_WINDOW_CAPTION); + } + + private void onPopupClose() { + uploadLayout.setCurrentUploadConfirmationwindow(null); + } + + private HorizontalLayout getFooterLayout() { + final HorizontalLayout footer = new HorizontalLayout(); + footer.setSizeUndefined(); + footer.addStyleName("confirmation-window-footer"); + footer.setSpacing(true); + footer.setMargin(false); + footer.addComponents(uploadBtn, cancelBtn); + footer.setComponentAlignment(uploadBtn, Alignment.TOP_LEFT); + footer.setComponentAlignment(cancelBtn, Alignment.TOP_RIGHT); + return footer; + } + + public Window getUploadConfrimationWindow() { + return uploadConfrimationWindow; + } + + @Override + public void buttonClick(final ClickEvent event) { + if (event.getComponent().getId().equals(SPUIComponetIdProvider.UPLOAD_ARTIFACT_DETAILS_CLOSE)) { + uploadConfrimationWindow.close(); + } else if (event.getComponent().getId().equals(SPUIComponetIdProvider.UPLOAD_DISCARD_DETAILS_BUTTON)) { + uploadLayout.clearFileList(); + uploadConfrimationWindow.close(); + } else if (event.getComponent().getId().equals(SPUIComponetIdProvider.UPLOAD_BUTTON)) { + processArtifactUpload(); + } + + else if (event.getComponent().getId().startsWith(SPUIComponetIdProvider.UPLOAD_DELETE_ICON)) { + final String itemId = ((Button) event.getComponent()).getData().toString(); + final Item item = uploadDetailsTable.getItem(((Button) event.getComponent()).getData()); + final Long swId = (Long) item.getItemProperty(BASE_SOFTWARE_ID).getValue(); + final CustomFile customFile = (CustomFile) item.getItemProperty(CUSTOM_FILE).getValue(); + final String fileName = (String) item.getItemProperty(FILE_NAME).getValue(); + final Label warningIconLabel = (Label) item.getItemProperty(WARNING_ICON).getValue(); + final Boolean isWarningIconDisplayed = isWarningIcon(warningIconLabel); + if (isWarningIconDisplayed) { + modifyIconOfSameSwId(itemId, swId, fileName); + } else if (isErrorIcon(warningIconLabel)) { + redErrorLabelCount--; + } + reValidateOtherFileNamesOfSameBaseSw(((Button) event.getComponent()).getData(), swId, fileName); + enableOrDisableUploadBtn(); + + uploadDetailsTable.removeItem(((Button) event.getComponent()).getData()); + uploadLayout.getFileSelected().remove(customFile); + uploadLayout.updateActionCount(); + if (uploadDetailsTable.getItemIds().isEmpty()) { + uploadLayout.clearFileList(); + uploadConfrimationWindow.close(); + } + + } + + } + + private void processArtifactUpload() { + final List itemIds = (List) uploadDetailsTable.getItemIds(); + if (preUploadValidation(itemIds)) { + final ArtifactManagement artifactManagement = SpringContextHelper.getBean(ArtifactManagement.class); + Boolean refreshArtifactDetailsLayout = false; + for (final String itemId : itemIds) { + final String[] itemDet = itemId.split("/"); + final String baseSoftwareModuleNameVersion = itemDet[0]; + final String fileName = itemDet[1]; + final SoftwareModule bSoftwareModule = artifactUploadState.getBaseSwModuleList() + .get(baseSoftwareModuleNameVersion); + for (final CustomFile customFile : uploadLayout.getFileSelected()) { + final String baseSwModuleNameVersion = HawkbitCommonUtil.getFormattedNameVersion( + customFile.getBaseSoftwareModuleName(), customFile.getBaseSoftwareModuleVersion()); + if (customFile.getFileName().equals(fileName) + && baseSwModuleNameVersion.equals(baseSoftwareModuleNameVersion)) { + createLocalArtifact(itemId, customFile.getFilePath(), artifactManagement, bSoftwareModule); + } + } + refreshArtifactDetailsLayout = checkIfArtifactDetailsDispalyed(bSoftwareModule.getId()); + } + if (refreshArtifactDetailsLayout) { + uploadLayout.refreshArtifactDetailsLayout(artifactUploadState.getSelectedBaseSoftwareModule().get()); + } + uploadLayout.clearFileList(); + uploadConfrimationWindow.close(); + // call upload result window + currentUploadResultWindow = new UploadResultWindow(uploadResultList, i18n); + UI.getCurrent().addWindow(currentUploadResultWindow.getUploadResultsWindow()); + currentUploadResultWindow.getUploadResultsWindow().addCloseListener(event -> onResultDetailsPopupClose()); + uploadLayout.setResultPopupHeightWidth(Page.getCurrent().getBrowserWindowWidth(), + Page.getCurrent().getBrowserWindowHeight()); + } else { + uploadLayout.getUINotification() + .displayValidationError(uploadLayout.getI18n().get("message.error.noProvidedName")); + } + + } + + private void onResultDetailsPopupClose() { + currentUploadResultWindow = null; + } + + private void createLocalArtifact(final String itemId, final String filePath, + final ArtifactManagement artifactManagement, final SoftwareModule baseSw) { + final File newFile = new File(filePath); + final Item item = tabelContainer.getItem(itemId); + final String sha1Checksum = ((TextField) item.getItemProperty(SHA1_CHECKSUM).getValue()).getValue(); + final String md5Checksum = ((TextField) item.getItemProperty(MD5_CHECKSUM).getValue()).getValue(); + final String providedFileName = (String) item.getItemProperty(FILE_NAME).getValue(); + final CustomFile customFile = (CustomFile) item.getItemProperty(CUSTOM_FILE).getValue(); + final String[] itemDet = itemId.split("/"); + final String swModuleNameVersion = itemDet[0]; + + FileInputStream fis = null; + try { + fis = new FileInputStream(newFile); + artifactManagement.createLocalArtifact(fis, baseSw.getId(), providedFileName, + HawkbitCommonUtil.trimAndNullIfEmpty(md5Checksum), + HawkbitCommonUtil.trimAndNullIfEmpty(sha1Checksum), true, customFile.getMimeType()); + saveUploadStatus(providedFileName, swModuleNameVersion, SPUILabelDefinitions.SUCCESS, ""); + } catch (final FileNotFoundException e) { + saveUploadStatus(providedFileName, swModuleNameVersion, SPUILabelDefinitions.FAILED, e.getMessage()); + LOG.error(ARTIFACT_UPLOAD_EXCEPTION, e); + } catch (final ArtifactUploadFailedException e) { + saveUploadStatus(providedFileName, swModuleNameVersion, SPUILabelDefinitions.FAILED, e.getMessage()); + LOG.error(ARTIFACT_UPLOAD_EXCEPTION, e); + } catch (final InvalidSHA1HashException e) { + saveUploadStatus(providedFileName, swModuleNameVersion, SPUILabelDefinitions.FAILED, e.getMessage()); + LOG.error(ARTIFACT_UPLOAD_EXCEPTION, e); + } catch (final InvalidMD5HashException e) { + saveUploadStatus(providedFileName, swModuleNameVersion, SPUILabelDefinitions.FAILED, e.getMessage()); + LOG.error(ARTIFACT_UPLOAD_EXCEPTION, e); + } finally { + closeFileStream(fis, newFile); + } + } + + private void saveUploadStatus(final String fileName, final String baseSwModuleName, final String status, + final String message) { + final UploadStatus result = new UploadStatus(); + result.setFileName(fileName); + result.setBaseSwModuleName(baseSwModuleName); + result.setUploadResult(status); + result.setReason(message); + uploadResultList.add(result); + + } + + private void closeFileStream(final FileInputStream fis, final File newFile) { + + if (fis != null) { + try { + fis.close(); + } catch (final IOException e) { + LOG.error(ARTIFACT_UPLOAD_EXCEPTION, e); + } + } + if (newFile.exists() && !newFile.delete()) { + LOG.error("Could not delete temporary file: {}", newFile); + } + + } + + public Table getUploadDetailsTable() { + return uploadDetailsTable; + } + + public VerticalLayout getUploadArtifactDetails() { + return uploadArtifactDetails; + } + + public UploadResultWindow getCurrentUploadResultWindow() { + return currentUploadResultWindow; + } + + public void setCurrentUploadResultWindow(final UploadResultWindow currentUploadResultWindow) { + this.currentUploadResultWindow = currentUploadResultWindow; + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadHandler.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadHandler.java new file mode 100755 index 000000000..834246c63 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadHandler.java @@ -0,0 +1,380 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.upload; + +import java.io.IOException; +import java.io.OutputStream; + +import org.eclipse.hawkbit.repository.exception.ArtifactUploadFailedException; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.ui.artifacts.state.CustomFile; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SpringContextHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.server.StreamVariable; +import com.vaadin.ui.Upload; +import com.vaadin.ui.Upload.FailedEvent; +import com.vaadin.ui.Upload.FailedListener; +import com.vaadin.ui.Upload.FinishedEvent; +import com.vaadin.ui.Upload.FinishedListener; +import com.vaadin.ui.Upload.ProgressListener; +import com.vaadin.ui.Upload.Receiver; +import com.vaadin.ui.Upload.StartedEvent; +import com.vaadin.ui.Upload.StartedListener; +import com.vaadin.ui.Upload.SucceededEvent; +import com.vaadin.ui.Upload.SucceededListener; + +/** + * Implementation to read file selected for upload. both for {@link Upload} and + * {@link StreamVariable} upload variants. + * + * The handler manages the output to the user and at the same time ensures that + * the upload does not exceed the configued max file size. + * + * + * + * + * + * + */ +public class UploadHandler implements StreamVariable, Receiver, SucceededListener, FailedListener, FinishedListener, + ProgressListener, StartedListener { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private static final Logger LOG = LoggerFactory.getLogger(UploadHandler.class); + + private final long fileSize; + private final UploadLayout view; + private final UploadStatusInfoWindow infoWindow; + private final long maxSize; + private final Upload upload; + + private volatile String fileName = null; + private volatile String mimeType = null; + private volatile boolean interrupted = false; + private String failureReason; + private final I18N i18n; + + UploadHandler(final String fileName, final long fileSize, final UploadLayout view, + final UploadStatusInfoWindow infoWindow, final long maxSize, final Upload upload, final String mimeType) { + super(); + this.fileName = fileName; + this.fileSize = fileSize; + this.view = view; + this.infoWindow = infoWindow; + this.maxSize = maxSize; + this.upload = upload; + this.mimeType = mimeType; + this.i18n = SpringContextHelper.getBean(I18N.class); + + } + + /** + * Create stream for {@link StreamVariable} variant. + * + * @see com.vaadin.server.StreamVariable#getOutputStream() + */ + @Override + public final OutputStream getOutputStream() { + try { + return view.saveUploadedFileDetails(fileName, fileSize, mimeType); + } catch (final ArtifactUploadFailedException e) { + LOG.error("Atifact upload failed {} ", e); + failureReason = e.getMessage(); + interrupted = true; + return new NullOutputStream(); + } + } + + /** + * Create stream for {@link Upload} variant. + * + * @see com.vaadin.ui.Upload.Receiver#receiveUpload(java.lang.String, + * java.lang.String) + */ + @Override + public OutputStream receiveUpload(final String fileName, final String mimeType) { + this.fileName = fileName; + this.mimeType = mimeType; + try { + if (view.validate()) { + if (view.checkForDuplicate(fileName)) { + view.showDuplicateMessage(); + } else { + view.increaseNumberOfFileUploadsExpected(); + return view.saveUploadedFileDetails(fileName, 0, mimeType); + } + } + } catch (final ArtifactUploadFailedException e) { + LOG.error("Atifact upload failed {} ", e); + failureReason = e.getMessage(); + upload.interruptUpload(); + } + // if final validation fails ,final no upload ,return NullOutputStream + return new NullOutputStream(); + } + + /** + * + * Upload sucessfull for {@link Upload} variant. + * + * @see com.vaadin.ui.Upload.SucceededListener#uploadSucceeded(com.vaadin.ui.Upload.SucceededEvent) + */ + @Override + public void uploadSucceeded(final SucceededEvent event) { + LOG.debug("Streaming finished for file :{}", event.getFilename()); + view.updateFileSize(event.getFilename(), event.getLength()); + + // recorded that we now one more uploaded + view.increaseNumberOfFilesActuallyUpload(); + + // inform upload status window + infoWindow.uploadSucceeded(event.getFilename()); + } + + /** + * Upload finished for {@link StreamVariable} variant. Called only in good + * case. So a combination of {@link #uploadSucceeded(SucceededEvent)} and + * {@link #uploadFinished(FinishedEvent)}. + * + * @see com.vaadin.server.StreamVariable#streamingFinished(com.vaadin.server.StreamVariable.StreamingEndEvent) + */ + @Override + public void streamingFinished(final StreamingEndEvent event) { + LOG.debug("Streaming finished for file :{}", event.getFileName()); + // record that we now one more uploaded + view.increaseNumberOfFilesActuallyUpload(); + + // inform upload status window + infoWindow.uploadSucceeded(event.getFileName()); + + // check if we are finished + if (view.enableProcessBtn()) { + infoWindow.uploadSessionFinished(); + } + view.updateActionCount(); + + // display the duplicate message after streaming all files + view.displayDuplicateMessageAfterStreamingAll(); + } + + /** + * Upload finished for {@link Upload} variant. Both for good and error + * variant. + * + * @see com.vaadin.ui.Upload.FinishedListener#uploadFinished(com.vaadin.ui.Upload.FinishedEvent) + */ + @Override + public void uploadFinished(final FinishedEvent event) { + LOG.debug("Upload finished for file :{}", event.getFilename()); + // check if we are finished + if (view.enableProcessBtn()) { + infoWindow.uploadSessionFinished(); + } + view.updateActionCount(); + + } + + /** + * Upload started for {@link StreamVariable} variant. + * + * @see com.vaadin.server.StreamVariable#streamingStarted(com.vaadin.server.StreamVariable.StreamingStartEvent) + */ + @Override + public void streamingStarted(final StreamingStartEvent event) { + LOG.debug("Streaming started for file :{}", fileName); + infoWindow.uploadStarted(fileName); + } + + /** + * Upload started for {@link Upload} variant. + * + * @see com.vaadin.ui.Upload.StartedListener#uploadStarted(com.vaadin.ui.Upload.StartedEvent) + */ + @Override + public void uploadStarted(final StartedEvent event) { + // single file session + if (view.isSoftwareModuleSelected() && !view.checkIfFileIsDuplicate(event.getFilename())) { + infoWindow.uploadSessionStarted(); + LOG.debug("Upload started for file :{}", event.getFilename()); + infoWindow.uploadStarted(event.getFilename()); + } else { + failureReason = i18n.get("message.upload.failed"); + upload.interruptUpload(); + } + } + + /** + * listen progress. + * + * @return boolean + */ + @Override + public boolean listenProgress() { + return true; + } + + /** + * Reports progress in {@link Upload} variant. + * + * @see com.vaadin.ui.Upload.ProgressListener#updateProgress(long, long) + */ + @Override + public void updateProgress(final long readBytes, final long contentLength) { + if (readBytes > maxSize || contentLength > maxSize) { + LOG.error("User tried to upload more than was allowed ({}).", maxSize); + view.decreaseNumberOfFileUploadsExpected(); + final SoftwareModule sw = view.getSoftwareModuleSelected(); + view.getFileSelected().remove(new CustomFile(fileName, sw.getName(), sw.getVersion())); + view.updateActionCount(); + failureReason = i18n.get("message.uploadedfile.size.exceeded", maxSize); + infoWindow.uploadFailed(fileName, failureReason); + upload.interruptUpload(); + interrupted = true; + return; + } + + infoWindow.updateProgress(fileName, readBytes, contentLength); + LOG.info("Update progress - {} : {}", fileName, (double) readBytes / (double) contentLength); + } + + /** + * Reports progress in {@link StreamVariable} variant. Interrupts + * + * @see com.vaadin.server.StreamVariable#onProgress(com.vaadin.server.StreamVariable.StreamingProgressEvent) + */ + @Override + public void onProgress(final StreamingProgressEvent event) { + if (event.getBytesReceived() > maxSize || event.getContentLength() > maxSize) { + LOG.error("User tried to upload more than was allowed ({}).", maxSize); + view.decreaseNumberOfFileUploadsExpected(); + final SoftwareModule sw = view.getSoftwareModuleSelected(); + view.getFileSelected().remove(new CustomFile(fileName, sw.getName(), sw.getVersion())); + view.updateActionCount(); + failureReason = i18n.get("message.uploadedfile.size.exceeded", maxSize); + infoWindow.uploadFailed(event.getFileName(), failureReason); + interrupted = true; + return; + } + + infoWindow.updateProgress(event.getFileName(), event.getBytesReceived(), event.getContentLength()); + // Logging to solve sonar issue + LOG.trace("Streaming in progress for file :{}", event.getFileName()); + } + + /** + * Upload failed for{@link StreamVariable} variant. + * + * @param event + * StreamingEndEvent + */ + @Override + public void streamingFailed(final StreamingErrorEvent event) { + LOG.info("Streaming failed for file :{}", event.getFileName()); + view.decreaseNumberOfFileUploadsExpected(); + final SoftwareModule sw = view.getSoftwareModuleSelected(); + view.getFileSelected().remove(new CustomFile(fileName, sw.getName(), sw.getVersion())); + view.updateActionCount(); + infoWindow.uploadFailed(event.getFileName(), failureReason); + // check if we are finished + if (view.enableProcessBtn()) { + infoWindow.uploadSessionFinished(); + } + view.displayDuplicateMessageAfterStreamingAll(); + + LOG.info("Streaming failed due to :{}", event.getException()); + } + + /** + * Upload failed for {@link Upload} variant. + * + * @see com.vaadin.ui.Upload.FailedListener#uploadFailed(com.vaadin.ui.Upload.FailedEvent) + */ + @Override + public void uploadFailed(final FailedEvent event) { + LOG.info("Upload failed for file :{}", event.getFilename()); + view.decreaseNumberOfFileUploadsExpected(); + /** + * If upload interrupted because of duplicate file,do not remove the + * file already in upload list + **/ + if (!view.getDuplicateFileNamesList().isEmpty()) { + final SoftwareModule sw = view.getSoftwareModuleSelected(); + view.getFileSelected().remove(new CustomFile(fileName, sw.getName(), sw.getVersion())); + } + view.updateActionCount(); + infoWindow.uploadFailed(event.getFilename(), failureReason); + + LOG.info("Upload failed for file :{}", event.getReason()); + + } + + /** + * to check if upload is interrupted. + */ + @Override + public boolean isInterrupted() { + return interrupted; + } + + private static class NullOutputStream extends OutputStream { + /** + * null output stream. + * + * @param i + * byte + */ + @Override + public void write(final int i) throws IOException { + // do nothing + } + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (fileName == null ? 0 : fileName.hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof UploadHandler)) { + return false; + } + final UploadHandler other = (UploadHandler) obj; + if (fileName == null && other.fileName != null) { + return false; + } else if (!fileName.equals(other.fileName)) { + return false; + } + return true; + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadLayout.java new file mode 100755 index 000000000..e570c84c4 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadLayout.java @@ -0,0 +1,598 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.upload; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.eclipse.hawkbit.repository.exception.ArtifactUploadFailedException; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleEvent; +import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleEvent.SoftwareModuleEventType; +import org.eclipse.hawkbit.ui.artifacts.event.UploadArtifactUIEvent; +import org.eclipse.hawkbit.ui.artifacts.state.ArtifactUploadState; +import org.eclipse.hawkbit.ui.artifacts.state.CustomFile; +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleSmall; +import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPUIComponetIdProvider; +import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; +import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; +import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; +import org.eclipse.hawkbit.ui.utils.UINotification; +import org.eclipse.hawkbit.util.SPInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.StringUtils; +import org.vaadin.spring.events.EventBus; +import org.vaadin.spring.events.EventScope; +import org.vaadin.spring.events.annotation.EventBusListenerMethod; + +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.DropHandler; +import com.vaadin.event.dd.acceptcriteria.AcceptAll; +import com.vaadin.event.dd.acceptcriteria.AcceptCriterion; +import com.vaadin.server.FontAwesome; +import com.vaadin.server.Page; +import com.vaadin.server.StreamVariable; +import com.vaadin.shared.ui.label.ContentMode; +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.Button; +import com.vaadin.ui.DragAndDropWrapper; +import com.vaadin.ui.DragAndDropWrapper.WrapperTransferable; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Html5File; +import com.vaadin.ui.Label; +import com.vaadin.ui.UI; +import com.vaadin.ui.Upload; +import com.vaadin.ui.VerticalLayout; + +/** + * Upload files layout. + * + * + */ +@ViewScope +@SpringComponent +public class UploadLayout extends VerticalLayout { + + private static final long serialVersionUID = -566164756606779220L; + + private static final Logger LOG = LoggerFactory.getLogger(UploadLayout.class); + + @Autowired + private UploadStatusInfoWindow uploadInfoWindow; + + @Autowired + private I18N i18n; + + @Autowired + private transient UINotification uiNotification; + + @Autowired + private transient EventBus.SessionEventBus eventBus; + + @Autowired + private ArtifactUploadState artifactUploadState; + + @Autowired + private transient SPInfo spInfo; + + private final AtomicInteger numberOfFileUploadsExpected = new AtomicInteger(); + + private final AtomicInteger numberOfFilesActuallyUpload = new AtomicInteger(); + + private final List duplicateFileNamesList = new ArrayList(); + + private Button processBtn; + + private Button discardBtn; + + private UploadConfirmationwindow currentUploadConfirmationwindow; + + private VerticalLayout dropAreaLayout; + + private UI ui; + + private HorizontalLayout fileUploadLayout; + + private DragAndDropWrapper dropAreaWrapper; + + /** + * Initialize the upload layout. + */ + @PostConstruct + void init() { + createComponents(); + buildLayout(); + updateActionCount(); + eventBus.subscribe(this); + ui = UI.getCurrent(); + } + + private void createComponents() { + + createProcessButton(); + createDiscardBtn(); + } + + private void buildLayout() { + + final Upload upload = new Upload(); + final UploadHandler uploadHandler = new UploadHandler(null, 0, this, uploadInfoWindow, + spInfo.getMaxArtifactFileSize(), upload, null); + upload.setButtonCaption(i18n.get("upload.file")); + upload.setImmediate(true); + upload.setReceiver(uploadHandler); + upload.addSucceededListener(uploadHandler); + upload.addFailedListener(uploadHandler); + upload.addFinishedListener(uploadHandler); + upload.addProgressListener(uploadHandler); + upload.addStartedListener(uploadHandler); + upload.addStyleName(SPUIStyleDefinitions.ACTION_BUTTON); + + fileUploadLayout = new HorizontalLayout(); + fileUploadLayout.setSpacing(true); + fileUploadLayout.addComponent(upload); + fileUploadLayout.setComponentAlignment(upload, Alignment.MIDDLE_LEFT); + fileUploadLayout.addComponent(processBtn); + fileUploadLayout.setComponentAlignment(processBtn, Alignment.MIDDLE_RIGHT); + fileUploadLayout.addComponent(discardBtn); + fileUploadLayout.setComponentAlignment(discardBtn, Alignment.MIDDLE_RIGHT); + setMargin(false); + + /* create drag-drop wrapper for drop area */ + dropAreaWrapper = new DragAndDropWrapper(createDropAreaLayout()); + dropAreaWrapper.setDropHandler(new DropAreahandler()); + setSizeFull(); + setSpacing(true); + + } + + public DragAndDropWrapper getDropAreaWrapper() { + return dropAreaWrapper; + } + + private class DropAreahandler implements DropHandler { + + private static final long serialVersionUID = 1L; + + @Override + public AcceptCriterion getAcceptCriterion() { + return AcceptAll.get(); + } + + @Override + public void drop(final DragAndDropEvent event) { + if (validate()) { + + final Html5File[] files = ((WrapperTransferable) event.getTransferable()).getFiles(); + if (files != null) { + for (final Html5File file : files) { + if (!checkForDuplicate(file.getFileName())) { + numberOfFileUploadsExpected.incrementAndGet(); + file.setStreamVariable(createStreamVariable(file)); + } + } + if (numberOfFileUploadsExpected.get() > 0) { + processBtn.setEnabled(false); + // reset before we start + uploadInfoWindow.uploadSessionStarted(); + } + // in case if all selected files are duplicate ,then display + // message + displayDuplicateMessageAfterStreamingAll(); + } + } + } + } + + private VerticalLayout createDropAreaLayout() { + dropAreaLayout = new VerticalLayout(); + final Label dropHereLabel = new Label("Drop files to upload"); + dropHereLabel.setWidth(null); + + final Label dropIcon = new Label(FontAwesome.ARROW_DOWN.getHtml(), ContentMode.HTML); + dropIcon.addStyleName("drop-icon"); + dropIcon.setWidth(null); + + dropAreaLayout.addComponent(dropIcon); + dropAreaLayout.setComponentAlignment(dropIcon, Alignment.BOTTOM_CENTER); + dropAreaLayout.addComponent(dropHereLabel); + dropAreaLayout.setComponentAlignment(dropHereLabel, Alignment.TOP_CENTER); + dropAreaLayout.setSizeFull(); + dropAreaLayout.setStyleName("upload-drop-area-layout-info"); + dropAreaLayout.setSpacing(false); + return dropAreaLayout; + } + + private void createProcessButton() { + processBtn = SPUIComponentProvider.getButton(SPUIComponetIdProvider.UPLOAD_PROCESS_BUTTON, + SPUILabelDefinitions.PROCESS, SPUILabelDefinitions.PROCESS, null, false, null, + SPUIButtonStyleSmall.class); + processBtn.setIcon(FontAwesome.BELL); + processBtn.addStyleName(SPUIStyleDefinitions.ACTION_BUTTON); + processBtn.addClickListener(event -> displayConfirmWindow(event)); + processBtn.setHtmlContentAllowed(true); + if (artifactUploadState.getFileSelected().isEmpty()) { + processBtn.setEnabled(false); + } + } + + private void createDiscardBtn() { + discardBtn = SPUIComponentProvider.getButton(SPUIComponetIdProvider.UPLOAD_DISCARD_BUTTON, + SPUILabelDefinitions.DISCARD, SPUILabelDefinitions.DISCARD, null, false, null, + SPUIButtonStyleSmall.class); + discardBtn.setIcon(FontAwesome.TRASH_O); + discardBtn.addStyleName(SPUIStyleDefinitions.ACTION_BUTTON); + discardBtn.addClickListener(event -> discardUploadData(event)); + } + + private StreamVariable createStreamVariable(final Html5File file) { + return new UploadHandler(file.getFileName(), file.getFileSize(), this, uploadInfoWindow, + spInfo.getMaxArtifactFileSize(), null, file.getType()); + } + + boolean checkForDuplicate(final String filename) { + final Boolean isDuplicate = checkIfFileIsDuplicate(filename); + if (isDuplicate) { + getDuplicateFileNamesList().add(filename); + } + return isDuplicate; + } + + @EventBusListenerMethod(scope = EventScope.SESSION) + void toggleProcessButton(final UploadArtifactUIEvent event) { + if (event == UploadArtifactUIEvent.ENABLE_PROCESS_BUTTON) { + processBtn.setEnabled(true); + } else if (event == UploadArtifactUIEvent.DISABLE_PROCESS_BUTTON) { + processBtn.setEnabled(false); + } + } + + /** + * Save uploaded file details. + * + * @param stream + * read from uploaded file + * @param name + * file name + * @param size + * file size + * @param mimeType + * the mimeType of the file + * @throws IOException + * in case of upload errors + */ + OutputStream saveUploadedFileDetails(final String name, final long size, final String mimeType) { + File tempFile = null; + try { + tempFile = File.createTempFile("spUiArtifactUpload", null); + + final OutputStream out = new FileOutputStream(tempFile); + + final SoftwareModule selectedSoftwareModule = artifactUploadState.getSelectedBaseSoftwareModule().get(); + + final String currentBaseSoftwareModuleKey = HawkbitCommonUtil + .getFormattedNameVersion(selectedSoftwareModule.getName(), selectedSoftwareModule.getVersion()); + + final CustomFile customFile = new CustomFile(name, size, tempFile.getAbsolutePath(), + selectedSoftwareModule.getName(), selectedSoftwareModule.getVersion(), mimeType); + + artifactUploadState.getFileSelected().add(customFile); + processBtn.setEnabled(false); + + if (!artifactUploadState.getBaseSwModuleList().keySet().contains(currentBaseSoftwareModuleKey)) { + artifactUploadState.getBaseSwModuleList().put(currentBaseSoftwareModuleKey, selectedSoftwareModule); + } + return out; + } catch (final FileNotFoundException e) { + LOG.error("Upload failed {}", e); + throw new ArtifactUploadFailedException(i18n.get("message.file.not.found")); + } catch (final IOException e) { + LOG.error("Upload failed {}", e); + throw new ArtifactUploadFailedException(i18n.get("message.upload.failed")); + } + } + + Boolean validate() { + if (!isSoftwareModuleSelected()) { + uiNotification.displayValidationError(i18n.get("message.error.noSwModuleSelected")); + + return false; + } + return true; + } + + SoftwareModule getSoftwareModuleSelected() { + if (artifactUploadState.getSelectedBaseSoftwareModule().isPresent()) { + return artifactUploadState.getSelectedBaseSoftwareModule().get(); + } + return null; + } + + Boolean isSoftwareModuleSelected() { + if (!artifactUploadState.getSelectedBaseSwModuleId().isPresent()) { + return false; + } + return true; + } + + /** + * Check if file selected is duplicate.i,e already selected for upload for + * same software module. + * + * @param name + * file name + * @return Boolean + */ + public Boolean checkIfFileIsDuplicate(final String name) { + Boolean isDuplicate = false; + final SoftwareModule selectedSoftwareModule = artifactUploadState.getSelectedBaseSoftwareModule().get(); + final String currentBaseSoftwareModuleKey = HawkbitCommonUtil + .getFormattedNameVersion(selectedSoftwareModule.getName(), selectedSoftwareModule.getVersion()); + + for (final CustomFile customFile : artifactUploadState.getFileSelected()) { + final String fileSoftwareModuleKey = HawkbitCommonUtil.getFormattedNameVersion( + customFile.getBaseSoftwareModuleName(), customFile.getBaseSoftwareModuleVersion()); + if (customFile.getFileName().equals(name) && currentBaseSoftwareModuleKey.equals(fileSoftwareModuleKey)) { + isDuplicate = true; + break; + } + } + return isDuplicate; + } + + void decreaseNumberOfFileUploadsExpected() { + numberOfFileUploadsExpected.decrementAndGet(); + } + + List getDuplicateFileNamesList() { + return duplicateFileNamesList; + } + + /** + * Update pending action count. + */ + void updateActionCount() { + if (!artifactUploadState.getFileSelected().isEmpty()) { + processBtn.setCaption(SPUILabelDefinitions.PROCESS + "
" + + artifactUploadState.getFileSelected().size() + "
"); + } else { + processBtn.setCaption(SPUILabelDefinitions.PROCESS); + } + } + + void displayDuplicateMessageAfterStreamingAll() { + // check if streaming of all dropped files are completed + if (numberOfFilesActuallyUpload.intValue() == numberOfFileUploadsExpected.intValue()) { + showDuplicateMessage(); + } + } + + /** + * Show duplicate file selected message. + */ + public void showDuplicateMessage() { + if (!duplicateFileNamesList.isEmpty()) { + final String fileNames = StringUtils.collectionToCommaDelimitedString(duplicateFileNamesList); + if (duplicateFileNamesList.size() == 1) { + uiNotification.displayValidationError(i18n.get("message.no.duplicateFile") + fileNames); + + } else if (duplicateFileNamesList.size() > 1) { + uiNotification.displayValidationError(i18n.get("message.no.duplicateFiles")); + } + duplicateFileNamesList.clear(); + } + } + + void increaseNumberOfFileUploadsExpected() { + numberOfFileUploadsExpected.incrementAndGet(); + } + + void updateFileSize(final String name, final long size) { + final SoftwareModule selectedSoftwareModule = artifactUploadState.getSelectedBaseSoftwareModule().get(); + final String currentBaseSoftwareModuleKey = HawkbitCommonUtil + .getFormattedNameVersion(selectedSoftwareModule.getName(), selectedSoftwareModule.getVersion()); + + for (final CustomFile customFile : artifactUploadState.getFileSelected()) { + final String fileSoftwareModuleKey = HawkbitCommonUtil.getFormattedNameVersion( + customFile.getBaseSoftwareModuleName(), customFile.getBaseSoftwareModuleVersion()); + if (customFile.getFileName().equals(name) && currentBaseSoftwareModuleKey.equals(fileSoftwareModuleKey)) { + customFile.setFileSize(size); + break; + } + } + } + + void increaseNumberOfFilesActuallyUpload() { + numberOfFilesActuallyUpload.incrementAndGet(); + } + + /** + * Enable process button once upload is completed. + */ + boolean enableProcessBtn() { + if (numberOfFilesActuallyUpload.intValue() >= numberOfFileUploadsExpected.intValue()) { + processBtn.setEnabled(true); + numberOfFileUploadsExpected.set(0); + numberOfFilesActuallyUpload.set(0); + return true; + } + return false; + } + + Set getFileSelected() { + return artifactUploadState.getFileSelected(); + } + + private void discardUploadData(final Button.ClickEvent event) { + if (event.getButton().equals(discardBtn)) { + if (artifactUploadState.getFileSelected().isEmpty()) { + uiNotification.displayValidationError(i18n.get("message.error.noFileSelected")); + + } else { + clearFileList(); + } + } + } + + /** + * Clear details. + */ + void clearFileList() { + // delete file system zombies + artifactUploadState.getFileSelected().forEach(customFile -> { + final File file = new File(customFile.getFilePath()); + file.delete(); + }); + + artifactUploadState.getFileSelected().clear(); + artifactUploadState.getBaseSwModuleList().clear(); + processBtn.setCaption(SPUILabelDefinitions.PROCESS); + /* disable when there is no files to upload. */ + processBtn.setEnabled(false); + numberOfFileUploadsExpected.set(0); + numberOfFilesActuallyUpload.set(0); + duplicateFileNamesList.clear(); + } + + private void setConfirmationPopupHeightWidth(final float newWidth, final float newHeight) { + if (currentUploadConfirmationwindow != null) { + currentUploadConfirmationwindow.getUploadArtifactDetails().setWidth(HawkbitCommonUtil + .getArtifactUploadPopupWidth(newWidth, SPUIDefinitions.MIN_UPLOAD_CONFIRMATION_POPUP_WIDTH), + Unit.PIXELS); + currentUploadConfirmationwindow.getUploadDetailsTable().setHeight(HawkbitCommonUtil + .getArtifactUploadPopupHeight(newHeight, SPUIDefinitions.MIN_UPLOAD_CONFIRMATION_POPUP_HEIGHT), + Unit.PIXELS); + } + } + + /** + * Set artifact upload result pop up size changes. + * + * @param newWidth + * new width of result pop up + * @param newHeight + * new height of result pop up + */ + void setResultPopupHeightWidth(final float newWidth, final float newHeight) { + if (currentUploadConfirmationwindow != null + && currentUploadConfirmationwindow.getCurrentUploadResultWindow() != null) { + final UploadResultWindow uploadResultWindow = currentUploadConfirmationwindow + .getCurrentUploadResultWindow(); + uploadResultWindow.getUploadResultsWindow().setWidth(HawkbitCommonUtil.getArtifactUploadPopupWidth(newWidth, + SPUIDefinitions.MIN_UPLOAD_CONFIRMATION_POPUP_WIDTH), Unit.PIXELS); + uploadResultWindow.getUploadResultTable().setHeight(HawkbitCommonUtil.getArtifactUploadPopupHeight( + newHeight, SPUIDefinitions.MIN_UPLOAD_CONFIRMATION_POPUP_HEIGHT), Unit.PIXELS); + } + } + + private void displayConfirmWindow(final Button.ClickEvent event) { + if (event.getComponent().getId().equals(SPUIComponetIdProvider.UPLOAD_PROCESS_BUTTON)) { + if (artifactUploadState.getFileSelected().isEmpty()) { + uiNotification.displayValidationError(i18n.get("message.error.noFileSelected")); + } else { + currentUploadConfirmationwindow = new UploadConfirmationwindow(this, artifactUploadState); + UI.getCurrent().addWindow(currentUploadConfirmationwindow.getUploadConfrimationWindow()); + setConfirmationPopupHeightWidth(Page.getCurrent().getBrowserWindowWidth(), + Page.getCurrent().getBrowserWindowHeight()); + } + } + } + + /** + * @return + */ + I18N getI18n() { + return i18n; + } + + /** + * @return + */ + SPInfo getSPInfo() { + return spInfo; + } + + void setCurrentUploadConfirmationwindow(final UploadConfirmationwindow currentUploadConfirmationwindow) { + this.currentUploadConfirmationwindow = currentUploadConfirmationwindow; + } + + /** + * @return + */ + + VerticalLayout getDropAreaLayout() { + return dropAreaLayout; + } + + @EventBusListenerMethod(scope = EventScope.SESSION) + void onEvent(final UploadArtifactUIEvent event) { + if (event == UploadArtifactUIEvent.DELETED_ALL_SOFWARE) { + ui.access(() -> updateActionCount()); + } + } + + @PreDestroy + void destroy() { + /* + * It's good manners to do this, even though vaadin-spring will + * automatically unsubscribe when this UI is garbage collected. + */ + eventBus.unsubscribe(this); + } + + /** + * set upload status and confirmation window. + * + * @param newWidth + * browser width + * @param newHeight + * browser height + */ + public void setUploadPopupSize(final float newWidth, final float newHeight) { + setConfirmationPopupHeightWidth(newWidth, newHeight); + setResultPopupHeightWidth(newWidth, newHeight); + } + + /** + * @param selectedBaseSoftwareModule + */ + public void refreshArtifactDetailsLayout(final SoftwareModule selectedBaseSoftwareModule) { + eventBus.publish(this, + new SoftwareModuleEvent(SoftwareModuleEventType.ARTIFACTS_CHANGED, selectedBaseSoftwareModule)); + } + + /** + * @return the fileUploadLayout + */ + public HorizontalLayout getFileUploadLayout() { + return fileUploadLayout; + } + + public UINotification getUINotification() { + return uiNotification; + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadResultWindow.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadResultWindow.java new file mode 100755 index 000000000..a4470b8e1 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadResultWindow.java @@ -0,0 +1,198 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.upload; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleTiny; +import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPUIComponetIdProvider; +import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; +import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; +import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; + +import com.vaadin.data.Item; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.Button; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.Table; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; +import com.vaadin.ui.themes.ValoTheme; + +/** + * Upload status popup. + * + * + * + * + * + */ +public class UploadResultWindow implements Button.ClickListener { + + private static final long serialVersionUID = 5205927189362269027L; + + private List uploadResultList = new ArrayList(); + + private Button closeBtn; + + private Table uploadResultTable; + + private Window uploadResultsWindow; + + private IndexedContainer tabelContainer; + + private final I18N i18n; + + private static final String FILE_NAME = "fileName"; + + private static final String BASE_SW_MODULE = "baseSwModuleName"; + + private static final String UPLOAD_RESULT = "uploadResult"; + + private static final String REASON = "reason"; + + /** + * Initialize upload status popup. + * + * @param uploadResultList + * upload status details + * @param i18n + * I18N + */ + public UploadResultWindow(final List uploadResultList, final I18N i18n) { + this.uploadResultList = uploadResultList; + this.i18n = i18n; + createComponents(); + createLayout(); + } + + private void createComponents() { + closeBtn = SPUIComponentProvider.getButton(SPUIComponetIdProvider.UPLOAD_ARTIFACT_RESULT_CLOSE, + SPUILabelDefinitions.CLOSE, SPUILabelDefinitions.CLOSE, ValoTheme.BUTTON_PRIMARY, false, null, + SPUIButtonStyleTiny.class); + closeBtn.addClickListener(this); + + uploadResultTable = new Table(); + uploadResultTable.addStyleName("artifact-table"); + uploadResultTable.setSizeFull(); + uploadResultTable.setImmediate(true); + uploadResultTable.setId(SPUIComponetIdProvider.UPLOAD_RESULT_TABLE); + uploadResultTable.addStyleName(ValoTheme.TABLE_BORDERLESS); + uploadResultTable.addStyleName(ValoTheme.TABLE_SMALL); + uploadResultTable.addStyleName(ValoTheme.TABLE_NO_VERTICAL_LINES); + uploadResultTable.addStyleName("accordion-tab-table-style"); + + populateUploadResultTable(); + } + + private void populateUploadResultTable() { + setTableContainer(); + Label statusLabel; + Label reasonLabel; + for (final UploadStatus uploadResult : uploadResultList) { + final Item newItem = tabelContainer + .addItem(uploadResult.getBaseSwModuleName() + "/" + uploadResult.getFileName()); + newItem.getItemProperty(FILE_NAME).setValue(HawkbitCommonUtil.getFormatedLabel(uploadResult.getFileName())); + newItem.getItemProperty(BASE_SW_MODULE) + .setValue(HawkbitCommonUtil.getFormatedLabel(uploadResult.getBaseSwModuleName())); + + if (uploadResult.getUploadResult().equals(SPUILabelDefinitions.SUCCESS)) { + statusLabel = new Label(HawkbitCommonUtil.getFormatedLabel(i18n.get("upload.success"))); + statusLabel.addStyleName("validation-success"); + newItem.getItemProperty(UPLOAD_RESULT).setValue(statusLabel); + } else { + statusLabel = new Label(HawkbitCommonUtil.getFormatedLabel(i18n.get("upload.failed"))); + statusLabel.addStyleName("validation-failed"); + newItem.getItemProperty(UPLOAD_RESULT).setValue(statusLabel); + } + + reasonLabel = HawkbitCommonUtil.getFormatedLabel(uploadResult.getReason()); + reasonLabel.setDescription(uploadResult.getReason()); + final String idStr = SPUIComponetIdProvider.UPLOAD_ERROR_REASON + uploadResult.getBaseSwModuleName() + "/" + + uploadResult.getFileName(); + reasonLabel.setId(idStr); + newItem.getItemProperty(REASON).setValue(reasonLabel); + } + } + + private void setTableContainer() { + tabelContainer = new IndexedContainer(); + + tabelContainer.addContainerProperty(FILE_NAME, Label.class, null); + tabelContainer.addContainerProperty(BASE_SW_MODULE, Label.class, null); + tabelContainer.addContainerProperty(UPLOAD_RESULT, Label.class, null); + tabelContainer.addContainerProperty(REASON, Label.class, null); + + uploadResultTable.setContainerDataSource(tabelContainer); + uploadResultTable.setPageLength(10); + uploadResultTable.setColumnHeader(FILE_NAME, i18n.get("upload.file.name")); + uploadResultTable.setColumnHeader(BASE_SW_MODULE, i18n.get("upload.swModuleTable.header")); + uploadResultTable.setColumnHeader(UPLOAD_RESULT, i18n.get("upload.result.status")); + uploadResultTable.setColumnHeader(REASON, i18n.get("upload.reason")); + + uploadResultTable.setColumnExpandRatio(FILE_NAME, 0.2f); + uploadResultTable.setColumnExpandRatio(BASE_SW_MODULE, 0.2f); + uploadResultTable.setColumnExpandRatio(UPLOAD_RESULT, 0.12f); + uploadResultTable.setColumnExpandRatio(REASON, 0.48f); + final Object[] visibileColumn = { FILE_NAME, BASE_SW_MODULE, UPLOAD_RESULT, REASON }; + uploadResultTable.setVisibleColumns(visibileColumn); + } + + private void createLayout() { + final HorizontalLayout footer = new HorizontalLayout(); + footer.setSizeUndefined(); + footer.addStyleName("confirmation-window-footer"); + footer.setSpacing(true); + footer.setMargin(false); + footer.addComponents(closeBtn); + footer.setComponentAlignment(closeBtn, Alignment.TOP_CENTER); + + final VerticalLayout uploadResultDetails = new VerticalLayout(); + uploadResultDetails.setWidth(SPUIDefinitions.MIN_UPLOAD_CONFIRMATION_POPUP_WIDTH + "px"); + uploadResultDetails.addStyleName("confirmation-popup"); + uploadResultDetails.addComponent(uploadResultTable); + uploadResultDetails.setComponentAlignment(uploadResultTable, Alignment.MIDDLE_CENTER); + uploadResultDetails.addComponent(footer); + uploadResultDetails.setComponentAlignment(footer, Alignment.MIDDLE_CENTER); + + uploadResultsWindow = new Window(); + uploadResultsWindow.setContent(uploadResultDetails); + uploadResultsWindow.setResizable(Boolean.FALSE); + uploadResultsWindow.setClosable(Boolean.FALSE); + uploadResultsWindow.setDraggable(Boolean.TRUE); + uploadResultsWindow.setModal(true); + uploadResultsWindow.setCaption(SPUILabelDefinitions.UPLOAD_RESULT); + uploadResultsWindow.addStyleName(SPUIStyleDefinitions.CONFIRMATION_WINDOW_CAPTION); + } + + @Override + public void buttonClick(final ClickEvent event) { + if (event.getComponent().getId().equals(SPUIComponetIdProvider.UPLOAD_ARTIFACT_RESULT_CLOSE) + || event.getComponent().getId().equals(SPUIComponetIdProvider.UPLOAD_ARTIFACT_RESULT_POPUP_CLOSE)) { + uploadResultsWindow.close(); + } + + } + + public Window getUploadResultsWindow() { + return uploadResultsWindow; + } + + public Table getUploadResultTable() { + return uploadResultTable; + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadStatus.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadStatus.java new file mode 100755 index 000000000..239475596 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadStatus.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.upload; + +import java.io.Serializable; + +/** + * Artifact upload status. + * + * + * + * + * + */ +public class UploadStatus implements Serializable { + + private static final long serialVersionUID = 8500552533390925782L; + + private String uploadResult; + + private String reason; + + private String fileName; + + private String baseSwModuleName; + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getBaseSwModuleName() { + return baseSwModuleName; + } + + public void setBaseSwModuleName(String baseSwModuleName) { + this.baseSwModuleName = baseSwModuleName; + } + + public String getUploadResult() { + return uploadResult; + } + + public void setUploadResult(String uploadResult) { + this.uploadResult = uploadResult; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadStatusInfoWindow.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadStatusInfoWindow.java new file mode 100755 index 000000000..8b36ef39e --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadStatusInfoWindow.java @@ -0,0 +1,225 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.artifacts.upload; + +import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; + +import com.vaadin.data.Item; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.FontAwesome; +import com.vaadin.shared.ui.window.WindowMode; +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; +import com.vaadin.ui.Grid; +import com.vaadin.ui.Grid.SelectionMode; +import com.vaadin.ui.UI; +import com.vaadin.ui.Window; +import com.vaadin.ui.renderers.HtmlRenderer; +import com.vaadin.ui.renderers.ProgressBarRenderer; + +import elemental.json.JsonValue; + +/** + * Shows upload status during upload. + * + * + * + */ + +@ViewScope +@SpringComponent +public class UploadStatusInfoWindow extends Window implements Window.CloseListener, Window.WindowModeChangeListener { + + private static final String PROGRESS = "Progress"; + + private static final String FILE_NAME = "File name"; + + private static final String STATUS = "Status"; + + private static final String REASON = "Reason"; + + private static final long serialVersionUID = 1L; + + private final Grid grid; + + private final IndexedContainer uploads; + + private volatile boolean errorOccured = false; + + /** + * Default Constructor. + */ + UploadStatusInfoWindow() { + super("Upload Status"); + + addStyleName(SPUIStyleDefinitions.UPLOAD_INFO); + center(); + setImmediate(true); + setResizable(true); + setDraggable(true); + setClosable(true); + uploads = new IndexedContainer(); + uploads.addContainerProperty(STATUS, String.class, "Active"); + uploads.addContainerProperty(FILE_NAME, String.class, null); + uploads.addContainerProperty(PROGRESS, Double.class, 0D); + uploads.addContainerProperty(REASON, String.class, ""); + + grid = new Grid(uploads); + grid.addStyleName(SPUIStyleDefinitions.UPLOAD_STATUS_GRID); + grid.setSelectionMode(SelectionMode.NONE); + grid.getColumn(STATUS).setRenderer(new StatusRenderer()); + grid.getColumn(PROGRESS).setRenderer(new ProgressBarRenderer()); + setColumnWidth(); + grid.setFrozenColumnCount(4); + grid.setColumnOrder(STATUS, PROGRESS, FILE_NAME, REASON); + grid.setHeaderVisible(true); + grid.setImmediate(true); + setPopupSizeInMinMode(); + + setContent(grid); + addCloseListener(this); + addWindowModeChangeListener(this); + } + + private void setColumnWidth() { + grid.getColumn(STATUS).setWidth(70); + grid.getColumn(PROGRESS).setWidth(150); + grid.getColumn(FILE_NAME).setWidth(280); + grid.getColumn(REASON).setWidth(300); + } + + private void resetColumnWidth() { + grid.getColumn(STATUS).setWidthUndefined(); + grid.getColumn(PROGRESS).setWidthUndefined(); + grid.getColumn(FILE_NAME).setWidthUndefined(); + grid.getColumn(REASON).setWidthUndefined(); + + } + + private static class StatusRenderer extends HtmlRenderer { + + @Override + public JsonValue encode(final String value) { + + String result = ""; + switch (value) { + case "Finished": + result = "
" + FontAwesome.CHECK_CIRCLE.getHtml() + "
"; + break; + case "Failed": + result = "
" + FontAwesome.EXCLAMATION_CIRCLE.getHtml() + "
"; + break; + default: + result = "
"; + } + + return super.encode(result); + } + } + + /** + * Automatically close if not error has occured. + */ + void uploadSessionFinished() { + if (!errorOccured) { + close(); + } + + } + + void uploadSessionStarted() { + close(); + UI.getCurrent().addWindow(this); + center(); + } + + void uploadStarted(final String filename) { + final Item item = uploads.addItem(filename); + item.getItemProperty(FILE_NAME).setValue(filename); + grid.scrollToEnd(); + + } + + void updateProgress(final String filename, final long readBytes, final long contentLength) { + final Item item = uploads.getItem(filename); + if (item != null) { + item.getItemProperty(PROGRESS).setValue((double) readBytes / (double) contentLength); + + } + } + + /** + * Called when each file upload is success. + * + * @param filename + * of the uploaded file. + */ + public void uploadSucceeded(final String filename) { + final Item item = uploads.getItem(filename); + if (item != null) { + item.getItemProperty(STATUS).setValue("Finished"); + + } + } + + void uploadFailed(final String filename, final String errorReason) { + final Item item = uploads.getItem(filename); + if (item != null) { + if (!errorOccured) { + errorOccured = true; + } + item.getItemProperty(REASON).setValue(errorReason); + item.getItemProperty(STATUS).setValue("Failed"); + + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Window.CloseListener#windowClose(com.vaadin.ui.Window. + * CloseEvent) + */ + @Override + public void windowClose(final CloseEvent e) { + clearWindow(); + } + + private void clearWindow() { + errorOccured = false; + uploads.removeAllItems(); + setWindowMode(WindowMode.NORMAL); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Window.WindowModeChangeListener#windowModeChanged(com. + * vaadin.ui.Window. WindowModeChangeEvent) + */ + @Override + public void windowModeChanged(final WindowModeChangeEvent event) { + if (event.getWindow().getWindowMode() == WindowMode.MAXIMIZED) { + resetColumnWidth(); + grid.getColumn(STATUS).setExpandRatio(0); + grid.getColumn(PROGRESS).setExpandRatio(1); + grid.getColumn(FILE_NAME).setExpandRatio(2); + grid.getColumn(REASON).setExpandRatio(3); + grid.setSizeFull(); + } else { + setColumnWidth(); + setPopupSizeInMinMode(); + } + } + + private void setPopupSizeInMinMode() { + grid.setWidth(800, Unit.PIXELS); + grid.setHeight(510, Unit.PIXELS); + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/AbstractAcceptCriteria.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/AbstractAcceptCriteria.java new file mode 100755 index 000000000..4b8d1e28a --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/AbstractAcceptCriteria.java @@ -0,0 +1,187 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.common; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; +import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; + +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion; +import com.vaadin.server.Page; +import com.vaadin.ui.Component; +import com.vaadin.ui.DragAndDropWrapper; +import com.vaadin.ui.DragAndDropWrapper.WrapperTargetDetails; +import com.vaadin.ui.Table; + +/** + * Abstract class for Accept criteria. + * + * + * + * + */ +public abstract class AbstractAcceptCriteria extends ServerSideCriterion { + + private static final long serialVersionUID = 3218899104852691974L; + + private int previousRowCount; + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.acceptcriteria.AcceptCriterion#accept(com.vaadin. + * event.dd.DragAndDropEvent ) + */ + @Override + public boolean accept(final DragAndDropEvent dragEvent) { + final Component compsource = dragEvent.getTransferable().getSourceComponent(); + final Component compdestination = dragEvent.getTargetDetails().getTarget(); + final int typeVal = getMouseEventType(compdestination, dragEvent); + + showHideDropAreaHighlights(typeVal, compsource, dragEvent); + if (isValidDrop(compsource, compdestination)) { + return true; + } else { + // Display action not allowed notification for invalid drop + /* mouse event will be Event.ONMOUSEUP on drop */ + // From com.google.gwt.user.client.Event + if (typeVal == 8) { + invalidDrop(); + } + return false; + } + + } + + private int getMouseEventType(final Component compdestination, final DragAndDropEvent dragEvent) { + int typeVal = 0; + if (compdestination instanceof DragAndDropWrapper) { + final WrapperTargetDetails details = (WrapperTargetDetails) dragEvent.getTargetDetails(); + typeVal = details.getMouseEvent().getType(); + + } else if (compdestination instanceof Table && dragEvent.getTargetDetails().getData("mouseEvent") != null) { + final String mouseEventDetails = dragEvent.getTargetDetails().getData("mouseEvent").toString(); + final String[] strArray = mouseEventDetails.split("\\,"); + typeVal = Integer.parseInt(strArray[7]); + } + return typeVal; + } + + private void showHideDropAreaHighlights(final int typeVal, final Component compsource, + final DragAndDropEvent dragEvent) { + /* mouse event will be Event.ONMOUSEUP on drop */ + // From com.google.gwt.user.client.Event + if (typeVal == 8) { + hideDropHints(); + } else { + if (compsource instanceof Table) { + showRowCount(dragEvent, (Table) compsource); + } + analyseDragComponent(compsource); + } + } + + /** + * + * @param dragEvent + * @param compsource + */ + protected void showRowCount(final DragAndDropEvent dragEvent, final Table compsource) { + /* Show the number of rows that are dragging in the drag image */ + final Set targetSelectedList = (Set) compsource.getValue(); + if (previousRowCount != targetSelectedList.size()) { + previousRowCount = targetSelectedList.size(); + /* + * Prepare the hava script to add the + +
+ +
diff --git a/hawkbit-ui/src/main/resources/VAADIN/themes/hawkbit/styles.scss b/hawkbit-ui/src/main/resources/VAADIN/themes/hawkbit/styles.scss new file mode 100755 index 000000000..654cb1a61 --- /dev/null +++ b/hawkbit-ui/src/main/resources/VAADIN/themes/hawkbit/styles.scss @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +@import "customstyles/hawkbitvariables"; +@import "../hawkbit/hawkbittheme"; + +/* This file prefixes all rules with the theme name to avoid causing conflicts with other themes. */ +/* The actual styles should be defined in testvalothemedemo.scss */ +.hawkbit { + @include hawkbittheme; +} diff --git a/hawkbit-ui/src/main/resources/messages.properties b/hawkbit-ui/src/main/resources/messages.properties new file mode 100755 index 000000000..7e69f4db8 --- /dev/null +++ b/hawkbit-ui/src/main/resources/messages.properties @@ -0,0 +1,423 @@ +# +# Copyright (c) 2015 Bosch Software Innovations GmbH and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# + +######################################################################################### +# This is the messages_en.properties file +######################################################################################### + +# Button names prefix with - button +button.save = Save +button.delete = Delete +button.discard.all = Discard All +button.delete.all = Delete All +button.assign.all = Save Assign +button.actions = You have actions +button.no.actions = No actions +button.ok = OK +button.cancel = Cancel +button.upload.file = Upload File +bulk.targets.upload = Please upload csv file. +bulkupload.ds.name = DS Name + +# Headers prefix with - header +header.target.table=Targets +header.dist.table=Distributions +header.filter.tag=Filter by Tag +header.target.filter.tag=Filters +header.first.assignment.table = Targets +header.second.assignment.table = Distributions +header.dist.first.assignment.table = Distributions +header.dist.second.assignment.table = Software Modules +header.third.assignment.table = Discard +header.one.deletedist.table = Distribution Name +header.second.deletedist.table = Discard Changes +header.first.deletetarget.table = Target Name +header.second.deletetarget.table = Discard Changes +header.first.deleteswmodule.table = Delete software +header.first.delete.dist.type.table = DistributionSetType +header.second.delete.dist.type.table = Discard +header.first.delete.swmodule.type.table = Software Module Type +header.second.delete.swmodule.type.table = Discard +header.dist.twintable.selected=Selected +header.dist.twintable.available=Available +header.target.installed = Installed +header.target.assigned = Assigned + + +# Captions prefix with - caption +caption.action.history = Action history for {0} +caption.error = Error +caption.new.softwaremodule.application = Configure New Application +caption.new.softwaremodule.jvm = Configure New Runtime +caption.new.softwaremodule.os = Configure New OS +caption.filter.simple = Simple Filter +caption.filter.custom = Custom Filter + +caption.add.softwaremodule = Configure Software Module +caption.add.new.dist = Configure New Distribution +caption.update.dist = Configure Update Distribution +caption.add.tag = Configure Tag +caption.add.type = Configure Type +caption.add.new.target = Configure New Target +caption.update.target = Configure Update Target +caption.bulk.upload.targets = Bulk Upload +caption.softwares.distdetail.tab = Modules +caption.tags.tab = Tags +caption.logs.tab = Logs +caption.attributes.tab = Attributes +caption.types.tab = Types +caption.save.window = Action Details +caption.assign.dist.accordion.tab = Assign Software Module +caption.delete.dist.accordion.tab = Delete Distributions +caption.delete.target.accordion.tab = Delete Targets +caption.delete.swmodule.accordion.tab = Delete SW Modules +caption.delete.dist.set.type.accordion.tab = Delete Distribution Set Type +caption.delete.sw.module.type.accordion.tab = Delete Software Module Type +caption.attributes = Attributes +caption.panel.dist.installed = Installed distribution set +caption.panel.dist.assigned = Assigned distribution set +caption.soft.delete.confirmbox = Confirm Software Module Delete Action +caption.cancel.action.confirmbox = Confirm action cancellation +caption.forced.datefield = Force update at time +caption.force.action.confirmbox = Confirm Force Active Action + +caption.filter.delete.confirmbox = Confirm Filter Delete Action + +# Labels prefix with - label +label.dist.details.type = Type : +label.dist.details.name = Name : +label.dist.details.version = Version : +label.dist.details.vendor = Vendor : +label.dist.details.jvm = Runtime : +label.dist.details.ah = Application : +label.dist.details.os = OS : +label.modified.date = Last modified at : +label.modified.by = Last modified by : +label.created.at = Created at : +label.created.by = Created by : +label.target.count = Targets : +label.description = Description : +label.ip = Address : +label.type = Type : +label.assigned.type = Assignment type : +label.assigned.count = {0} Assigned +label.installed.count = {0} Installed +label.mandatory.field = * Mandatory Field +label.components.drop.area = Drop here to delete +label.software.module.drop.area = Delete Software +label.create.tag = Create Tag +label.update.tag = Update Tag +label.create.type = Create Type +label.update.type = Update Type +label.singleAssign.type = Firmware (FW) +label.multiAssign.type = Software (SW) +label.choose.type = Choose Type to Update +label.choose.type.color = Choose Type Color +label.combobox.type = Select Type +label.combobox.tag = Select Tag +label.choose.tag = Choose Tag to update +label.choose.tag.color = Choose Tag Color +label.filter = Filter: +label.target.filter.count = Total Targets: +label.filter.selected = Selected: +label.filter.shown = Shown: +label.filter.targets = Filtered targets : +label.filter.status = Status, +label.filter.tags = Tags, +label.filter.text = Search Text +label.filter.dist = Distribution, +label.filter.custom = Custom +label.target.filter.truncated={0} targets has been truncated in the list due the target size limit of {1}, use filters to reduce the targets to be shown +label.active =Active +label.inactive = In-active +label.finished = Finished +label.error = Error +label.warning = Warning +label.running = Running +label.cancelled = Cancelled +label.cancelling = Canceling +label.retrieved = Retrieved +label.download = Downloading +label.unknown = Unknown +label.target.id = Controller Id : +label.target.ip = Controller IP : +label.target.security.token = Security token : +label.filter.by.status = Filter by Status +label.target.controller.attrs = Controller attributes +label.target.lastpolldate = Last poll : +label.tag.name = Tag name +label.unsupported.browser.ie=Sorry! current browser is not supported. Please use Internet Explorer 11 and above + +# Checkbox label prefix with - checkbox +checkbox.dist.migration.required = Required Migration Step : +checkbox.dist.required.migration.step = Required Migration Step + +# TextFields prefix with - textfield +textfield.name = Name +textfield.key = Key +textfield.version = Version +textfield.vendor = Vendor +textfield.description = Description +textfield.customfiltername = Filter name +ui.version = Powered by Bosch IoT Software Provisioning +prompt.target.id = Controller ID + + +#Tooltips prefix with - tooltip +tooltip.add.module = Add Software Module +tooltip.status.unknown = Unknown +tooltip.status.registered = Registered +tooltip.status.pending = Pending +tooltip.status.error = Error +tooltip.status.insync = In-sync +tooltip.delete.module = Select and delete Software Module +tooltip.forced.item=Forced update action +tooltip.soft.item=Soft update action which interacts with an user as a attempt update action for example +tooltip.timeforced.item=Soft update until a specific time and then the action will be forced +tooltip.check.for.mandatory=Check to make Mandatory +tooltip.artifact.icon=Show Artifact Details +tooltip.click.to.edit = Click to edit + +# Notification messages prefix with - message +message.save.success = {0} saved successfully +message.update.success = {0} updated successfully +message.delete.success = {0} deleted successfully +message.dist.installedorassigned = Target {targId} is already assigned/installed with distribution +message.dist.pending.action = Target {0} is already assigned with distribution {1} . Pending for action +message.empty.target.tags= No Tags Created +message.select.row = Please select a row to drag +message.error = Unknown error occured during the operation. Please contact administrator +message.dist.assigned.one = {0} is assigned to {1} +message.dist.assigned.many = {0} DistributionSets are assigned to {1} +message.dist.unassigned.one = {0} is unassigned from {1} +message.dist.unassigned.many = {0} DistributionSets are unassigned from {1} +message.target.assigned.one = {0} is assigned to {1} +message.target.assigned.many = {0} Targets are assigned to {1} +message.target.unassigned.one = {0} is unassigned from {1} +message.target.unassigned.many = {0} Targets are unassigned from {1} +message.target.assigned.pending = Some target(s) are already assigned.Pending for action +message.cannot.delete = Cannot be deleted +message.check.softwaremodule = Please provide both name and verion! +message.duplicate.softwaremodule = {0} : {1} already exists! +message.tag.delete = Please unclick the tag {0}, then try to delete +message.dist.type.check.delete = Please unclick the distribution type {0}, then try to delete +message.swmodule.type.check.delete = Please unclick the software module type {0}, then try to delete +message.targets.already.deleted = Few Target(s) are already deleted.Pending for action +message.dists.already.deleted = Few distribution(s) are already deleted.Pending for action +message.target.deleted.pending = Target(s) already deleted.Pending for action +message.dist.deleted.pending = Distribution(s) already deleted.Pending for action +message.dist.delete.success = All selected distribution sets are deleted successfully ! +message.dist.discard.success = All distributions selected for delete are discarded successfully ! +message.target.delete.success = All selected targets are deleted successfully ! +message.target.discard.success = All targets selected for delete are discarded successfully ! +message.software.discard.success = All software modules selected for delete are discarded successfully ! +message.software.type.discard.success = All software moduleTypes selected for delete are discarded successfully ! +message.assign.software.discard.success = All software moduleTypes selected for assign are discarded successfully ! +message.software.delete.success = All software modules selected for delete are deleted successfully ! +message.software.type.delete.success = All software modules types selected for delete are deleted successfully ! +message.dist.set.type.deleted.success = {0} DistributionSetType deleted successfully ! +message.new.dist.save.success = {0} - {1} saved successfully +message.dist.update.success = {0} - {1} updated successfully +message.duplicate.dist = Distribution set [{0}] or version [{1}] must be unique, entered value already exists. +message.error.view = No such view: {0} +message.accessdenied.view = No access to view: {0} +message.no.data = No data available +message.target.assignment = {0} Assignment(s) done +message.target.deleted = {0} Target(s) deleted +message.dist.deleted = {0} Distribution set(s) deleted +message.tag.update.mandatory = Please select the Tag to update +message.tag.duplicate.check = {0} already exists, please enter another value +message.type.key.duplicate.check = Distribution type with key {0} already exists, please give another value +message.type.key.swmodule.duplicate.check = Software Module type with key {0} already exists, please give another value +message.no.action.history = No action history is available for the target : {0} +message.no.available = --No messages available-- +message.no.actionupdateds.available = No other updates available for this action +message.mandatory.check = Mandatory details are missing +message.target.duplicate.check = Target [ {0} ] must be unique, entered value already exists. +message.permission.insufficient = Insufficient permissions to perform this action. +message.error.temp = The operation cannot be fulfilled due to {0}. Please contact administrator +message.dist.alreadyassigned = {0} : {1} is already assigned/installed, cannot be updated +message.dist.tag.alreadyassigned = {0} : {1} is already assigned/installed, cannot assign/un-assign to tag +message.dists.unassign.tag.alreadyassigned = Few of the DistributionSet's are already assigned to Target, those cannot be unassigned from Tag" +message.dists.assign.tag.alreadyassigned = Few of the DistributionSet's are already assigned to Target, those cannot be assigned to Tag" +message.dists.tag.assigned = {0} DistributionSet's assigned to Tag {1} +message.dists.tag.unassigned = {0} DistributionSet's un-assigned from Tag {1} +message.dist.no.operation = {0} - already assigned/installed, No operation +message.sm.delete.confirm = Are you sure that you want to delete the selected {0} software module? +message.error.os.softmodule = Please select the OS to delete +message.error.ah.softmodule = Please select the Application to delete +message.error.softmodule.deleted = The selected software module is already deleted +message.cancel.action = Cancel +message.cancel.action.success = Action cancelled successfully ! +message.cancel.action.failed = Unable to cancel the action ! +message.cancel.action.confirm = Are you sure that you want to cancel this action? +message.target.alreadyAssigned = {0} Target(s) were already assigned +message.dist.alreadyAssigned = {0} Distribution Set(s) were already assigned +message.force.action = Force +message.force.action.confirm = Are you sure that you want to force this action? +message.force.action.success = Action forced successfully ! +message.distribution.no.update = distribution {0} set is already assigned to targets and cannot be changed +message.action.not.allowed = Action not allowed +message.onlyone.distribution.assigned = Only one distribution set can be assigned +message.onlyone.distribution.dropallowed = Only one distribution set can be dropped +message.error.missing.typename = Missing Type Name +message.error.missing.typenameorkey = Missing Type Name or Key or software module type +message.error.missing.typenameorkey = Missing Type Name or Key +message.tag.cannot.be.assigned = Target/DS cannot be assigned to {0} +message.no.targets.assiged.fortag = No targets are assigned to tag {0} +message.type.delete = Please unclick the distribution type {0}, then try to delete +message.target.ds.assign.success = Assignments saved successfully ! +message.no.directory.upload = Directory upload is not supported +message.delete.filter.confirm = Are you sure that you want to delete custom filter? +message.delete.filter.success = Custom filter {0} deleted Successfully! +message.create.filter.success = Custom filter {0} created Successfully! +message.update.filter.success = Custom filter updated Successfully! +message.target.filter.validation = Please enter name and query +message.target.filter.duplicate = {0} already exists, please enter another value + +# action info +action.target.table.selectall = Select all (Ctrl+A) +action.target.table.clear = Clear selections + +#reused messages +soft.module.jvm =Runtime +soft.module.application =Application +soft.module.os =OS + +#Artifact upload +message.error.noFileSelected = No file selected for upload +message.error.noProvidedName = Please provide custom file name +message.error.noSwModuleSelected = Please select a software module +message.no.duplicateFiles = duplicate files selected +message.no.duplicateFile = Duplicate file selected : +message.delete.artifact = Are you sure that you want to delete artifact {0} ? +message.duplicate.filename = Duplicate file name +message.swModule.deleted = {0} Software module(s) deleted +message.upload.failed = Streaming Failed +message.uploadedfile.size.exceeded = File size exceeded .Allowed size {0} bytes +message.file.not.found = File not found + +upload.swModuleTable.header = Software module +upload.selectedfile.name = file selected for upload +upload.file.name = File name +upload.sha1 = SHA1 checksum +upload.md5 = MD5 checksum +upload.last.modified.date=Last modified date +upload.failed = Failed +upload.success = Success +upload.caption.add.new.swmodule = Configure New Software Module +upload.caption.delete.swmodule = Configure Delete Software Module +upload.swmodule.type = Type +upload.artifact.alreadyExists = Artifact will be overridden as the given name already exists +upload.size = Size(B) +upload.validation = Validation +upload.reason = Reason +upload.action = Action +upload.result.status = Upload status +upload.file = Upload File +upload.caption.update.swmodule = Update softwate module +caption.tab.details = Details +caption.tab.description = Description + +caption.delete.artifact.confirmbox = Confirm Artifact Delete Action + +custom.filter.name = Filter Name +custom.filter.created.by = Created By +custom.created.date = Created Date + +#Manage distributions view +label.drop.dist.delete.area = Drop here
to delete +label.no.tag.assigned = NO TAG +caption.assign.software.dist.accordion.tab = Assign Software Modules +message.software.assignment = {0} Software Module(s) Assignment(s) done +message.dist.inuse = {0} Distribution is already assigned to target +message.software.dist.already.assigned = {0} Distribution already has software module {1} +message.software.dist.type.notallowed = {0} Software module type can not assign to Distribution {1} +message.target.assigned = {0} is assigned to {1} +message.dist.type.delete = {0} DistributionType(s) Deleted successfully. +message.sw.module.type.delete = {0} Software Module Type(s) deleted successfully. +message.dist.type.discard.success = All Distribution Types are discarded successfully ! +message.dist.discard.success = All Distributions are discarded successfully ! +message.assign.discard.success = All assignments are discarded successfully ! +message.target.ds.assign.success = Assignments saved successfully ! + +# Login view +notification.login.title=Welcome to Bosch IoT Software Provisioning. +notification.login.description=Please login with your Bosch Identity Management credentials. +notification.login.failed.title=Login failed! +notification.login.failed.description=Login with the given credentials failed. +notification.login.failed.credentialsexpired.title=Password Expired! +notification.login.failed.credentialsexpired.description=Password has been expired or needs to be set initially, please visit the User Management to change or set your password. +label.login.tenant=Tenant +label.login.username=Username +label.login.password=Password +button.login.signin=Sign in +checkbox.login.rememberme=Remember me + +# Links +link.documentation.name=Documentation +link.demo.name=Demo +link.requestaccount.name=Request Account +link.support.name=Support +link.usermanagement.name=User Management + +# System Configuration View +notification.configuration.save=Saved changes +configuration.defaultdistributionset.title=Distribution Set Type +configuration.defaultdistributionset.select.label=Select the default Distribution Set type: +configuration.savebutton.tooltip=Save Configurations +configuration.cancellbutton.tooltip=Cancel Configurations +configuration.authentication.title=Authentication Configuration + +#Calendar +calendar.year=year +calendar.years=years +calendar.month=month +calendar.months=months +calendar.day=day +calendar.days=days +calendar.hour=hour +calendar.hours=hours +calendar.minute=minute +calendar.minutes=minutes +calendar.second=second +calendar.seconds=seconds + + +header.name = Name +header.vendor = Vendor +header.version = Version +header.description = Description +header.createdBy = Created By +header.createdDate = Created Date +header.modifiedBy = Modified By +header.modifiedDate = Modified Date +header.delete = Delete +header.assigned.ds = Assigned DS +header.installed.ds = Installed DS +header.target.status = Status +header.target.tags = Tags + + + + +distribution.details.header = Distribution set +target.details.header = Target +header.caption.mandatory = Mandatory +header.caption.typename = SoftwareModuleType +header.caption.softwaremodule = SoftwareModule +header.caption.upload.details = Upload details +combo.type.tag.name = Type tag name + +label.yes = Yes +label.no = No + +#Menu +menu.title = Software Provisioning + diff --git a/hawkbit-ui/src/main/resources/messages_de.properties b/hawkbit-ui/src/main/resources/messages_de.properties new file mode 100755 index 000000000..35c69240e --- /dev/null +++ b/hawkbit-ui/src/main/resources/messages_de.properties @@ -0,0 +1,414 @@ +# +# Copyright (c) 2015 Bosch Software Innovations GmbH and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# + +######################################################################################### +# This is the messages_en.properties file +######################################################################################### + +# Button names prefix with - button +button.save = Save +button.delete = Delete +button.discard.all = Discard All +button.delete.all = Delete All +button.assign.all = Save Assign +button.actions = You have actions +button.no.actions = No actions +button.ok = OK +button.cancel = Cancel +button.upload.file = Upload File +bulk.targets.upload = Please upload csv file. +bulkupload.ds.name = DS Name + +# Headers prefix with - header +header.target.table=Targets +header.dist.table=Distributions +header.filter.tag=Filter by Tag +header.target.filter.tag=Filters +header.first.assignment.table = Targets +header.second.assignment.table = Distributions +header.dist.first.assignment.table = Distributions +header.dist.second.assignment.table = Software Modules +header.third.assignment.table = Discard +header.one.deletedist.table = Distribution Name +header.second.deletedist.table = Discard Changes +header.first.deletetarget.table = Target Name +header.second.deletetarget.table = Discard Changes +header.first.deleteswmodule.table = Delete software +header.first.delete.dist.type.table = DistributionSetType +header.second.delete.dist.type.table = Discard +header.first.delete.swmodule.type.table = Software Module Type +header.second.delete.swmodule.type.table = Discard +header.dist.twintable.selected=Selected +header.dist.twintable.available=Available +header.target.installed = Installed +header.target.assigned = Assigned + +# Captions prefix with - caption +caption.action.history = Action history for {0} +caption.error = Error +caption.new.softwaremodule.application = Configure New Application +caption.new.softwaremodule.jvm = Configure New Runtime +caption.new.softwaremodule.os = Configure New OS + +caption.add.softwaremodule = Configure Software Module +caption.add.new.dist = Configure New Distribution +caption.update.dist = Configure Update Distribution +caption.add.tag = Configure Tag +caption.add.type = Configure Type +caption.add.new.target = Configure New Target +caption.update.target = Configure Update Target +caption.bulk.upload.targets = Bulk Upload +caption.softwares.distdetail.tab = Modules +caption.tags.tab = Tags +caption.logs.tab = Logs +caption.attributes.tab = Attributes +caption.types.tab = Types +caption.save.window = Action Details +caption.assign.dist.accordion.tab = Assign Distribution Set +caption.delete.dist.accordion.tab = Delete Distributions +caption.delete.target.accordion.tab = Delete Targets +caption.delete.swmodule.accordion.tab = Delete SW Modules +caption.delete.dist.set.type.accordion.tab = Delete Distribution Set Type +caption.delete.sw.module.type.accordion.tab = Delete Software Module Type +caption.attributes = Attributes +caption.panel.dist.installed = Installed distribution set +caption.panel.dist.assigned = Assigned distribution set +caption.soft.delete.confirmbox = Confirm Software Module Delete Action +caption.cancel.action.confirmbox = Confirm action cancellation +caption.forced.datefield = Force update at time +caption.force.action.confirmbox = Confirm Force Active Action +caption.filter.simple = Simple Filter +caption.filter.custom = Custom Filter + +caption.filter.delete.confirmbox = Confirm Filter Delete Action + +# Labels prefix with - label +label.dist.details.type = Type : +label.dist.details.name = Name : +label.dist.details.version = Version : +label.dist.details.vendor = Vendor : +label.dist.details.jvm = Runtime : +label.dist.details.ah = Application : +label.dist.details.os = OS : +label.modified.date = Last modified at : +label.modified.by = Last modified by : +label.created.at = Created at : +label.created.by = Created by : +label.target.count = Targets : +label.description = Description : +label.ip = Address : +label.type = Type : +label.assigned.type = Assignment type : +label.assigned.count = {0} Assigned +label.installed.count = {0} Installed +label.mandatory.field = * Mandatory Field +label.components.drop.area = Drop here to delete +label.software.module.drop.area = Delete Software +label.create.tag = Create Tag +label.update.tag = Update Tag +label.create.type = Create Type +label.update.type = Update Type +label.singleAssign.type = Firmware (FW) +label.multiAssign.type = Software (SW) +label.choose.type = Choose Type +label.choose.type.color = Choose Type Color +label.combobox.type = Select Type +label.combobox.tag = Select Tag +label.choose.tag = Choose Tag to update +label.choose.tag.color = Choose Tag Color +label.filter = Filter: +label.target.filter.count = Total Targets: +label.filter.selected = Selected: +label.filter.shown = Shown: +label.filter.status = Status, +label.filter.tags = Tags, +label.filter.text = Search Text +label.filter.dist = Distribution, +label.filter.custom = Custom +label.target.filter.truncated={0} targets has been truncated in the list due the target size limit of {1}, use filters to reduce the targets to be shown +label.active =Active +label.inactive = In-active +label.finished = Finished +label.error = Error +label.warning = Warning +label.running = Running +label.cancelled = Cancelled +label.cancelling = Canceling +label.retrieved = Retrieved +label.download = Downloading +label.target.id = Controller Id : +label.target.ip = Controller IP : +label.target.security.token = Security token : +label.filter.by.status = Filter by Status +label.target.controller.attrs = Controller attributes +label.target.lastpolldate = Last poll : +label.no.tag.assigned = NO TAG +label.tag.name = Tag name +label.configuration.auth.header = Allow targets to authenticate via a certificate authenticated by an reverse proxy +label.configuration.auth.gatewaytoken = Allow a gateway to authenticate and manage multiple targets through a gateway security token +label.configuration.auth.targettoken = Allow targets to authenticate directly with their target security token +label.unsupported.browser.ie=Sorry! current browser is not supported. Please use Internet Explorer 11 and above + +# Checkbox label prefix with - checkbox +checkbox.dist.migration.required = Required Migration Step : +checkbox.dist.required.migration.step = Required Migration Step + +# TextFields prefix with - textfield +textfield.name = Name +textfield.key = Key +textfield.version = Version +textfield.vendor = Vendor +textfield.description = Description +textfield.customfiltername = Filter name +ui.version = Powered by Bosch IoT Software Provisioning +prompt.target.id = Controller ID + + +#Tooltips prefix with - tooltip +tooltip.add.module = Add Software Module +tooltip.status.unknown = Unknown +tooltip.status.registered = Registered +tooltip.status.pending = Pending +tooltip.status.error = Error +tooltip.status.insync = In-sync +tooltip.delete.module = Select and delete Software Module +tooltip.forced.item=Forced update action +tooltip.soft.item=Soft update action which interacts with an user as a attempt update action for example +tooltip.timeforced.item=Soft update until a specific time and then the action will be forced +tooltip.check.for.mandatory=Check to make Mandatory +tooltip.artifact.icon=Show Artifact Details +tooltip.click.to.edit = Click to edit + + +# Notification messages prefix with - message +message.save.success = {0} saved successfully +message.update.success = {0} updated successfully +message.delete.success = {0} deleted successfully +message.dist.installedorassigned = Target {targId} is already assigned/installed with distribution +message.dist.pending.action = Target {0} is already assigned with distribution {1} . Pending for action +message.empty.target.tags= No Tags Created +message.empty.disttype.tags = No Distribution type tags created +message.select.row = Please select a row to drag +message.error = Unknown error occured during the operation. Please contact administrator +message.dist.assigned.one = {0} is assigned to {1} +message.dist.assigned.many = {0} DistributionSets are assigned to {1} +message.dist.unassigned.one = {0} is unassigned from {1} +message.dist.unassigned.many = {0} DistributionSets are unassigned from {1} +message.target.assigned.one = {0} is assigned to {1} +message.target.assigned.many = {0} Targets are assigned to {1} +message.target.unassigned.one = {0} is unassigned from {1} +message.target.unassigned.many = {0} Targets are unassigned from {1} +message.target.assigned.pending = Some target(s) are already assigned.Pending for action +message.cannot.delete = Cannot be deleted +message.check.softwaremodule = Please provide both name and verion! +message.duplicate.softwaremodule = {0} : {1} already exists! +message.tag.delete = Please unclick the tag {0}, then try to delete +message.dist.type.check.delete = Please unclick the distribution type {0}, then try to delete +message.swmodule.type.check.delete = Please unclick the software module type {0}, then try to delete +message.targets.already.deleted = Few Target(s) are already deleted.Pending for action +message.dists.already.deleted = Few distribution(s) are already deleted.Pending for action +message.target.deleted.pending = Target(s) already deleted.Pending for action +message.dist.deleted.pending = Distribution(s) already deleted.Pending for action +message.dist.delete.success = All selected distribution sets are deleted successfully ! +message.dist.discard.success = All distributions selected for delete are discarded successfully ! +message.target.delete.success = All selected targets are deleted successfully ! +message.target.discard.success = All targets selected for delete are discarded successfully ! +message.software.discard.success = All software modules selected for delete are discarded successfully ! +message.software.type.discard.success = All software moduleTypes selected for delete are discarded successfully ! +message.assign.software.discard.success = All software moduleTypes selected for assign are discarded successfully ! +message.software.delete.success = All software modules selected for delete are deleted successfully ! +message.software.type.delete.success = All software modules types selected for delete are deleted successfully ! +message.dist.set.type.deleted.success = {0} DistributionSetType deleted successfully ! +message.new.dist.save.success = {0} - {1} saved successfully +message.dist.update.success = {0} - {1} updated successfully +message.duplicate.dist = Distribution set [{0}] or version [{1}] must be unique, entered value already exists. +message.error.view = No such view: {0} +message.accessdenied.view = No access to view: {0} +message.no.data = No data available +message.target.assignment = {0} Assignment(s) done +message.target.deleted = {0} Target(s) deleted +message.dist.deleted = {0} Distribution set(s) deleted +message.tag.update.mandatory = Please select the Tag to update +message.tag.duplicate.check = {0} already exists, please enter another value +message.type.key.duplicate.check = Distribution type with key {0} already exists, please give another value +message.type.key.swmodule.duplicate.check = Software Module type with key {0} already exists, please give another value +message.no.action.history = No action history is available for the target : {0} +message.no.available = --No messages available-- +message.no.actionupdateds.available = No other updates available for this action +message.mandatory.check = Mandatory details are missing +message.target.duplicate.check = Target [ {0} ] must be unique, entered value already exists. +message.permission.insufficient = Insufficient permissions to perform this action. +message.error.temp = The operation cannot be fulfilled due to {0}. Please contact administrator +message.dist.alreadyassigned = {0} : {1} is already assigned/installed, cannot be updated +message.dist.tag.alreadyassigned = {0} : {1} is already assigned/installed, cannot assign/un-assign to tag +message.dists.unassign.tag.alreadyassigned = Few of the DistributionSet's are already assigned to Target, those cannot be unassigned from Tag" +message.dists.assign.tag.alreadyassigned = Few of the DistributionSet's are already assigned to Target, those cannot be assigned to Tag" +message.dists.tag.assigned = {0} DistributionSet's assigned to Tag {1} +message.dists.tag.unassigned = {0} DistributionSet's un-assigned from Tag {1} +message.dist.no.operation = {0} - already assigned/installed, No operation +message.sm.delete.confirm = Are you sure that you want to delete the selected {0} software module? +message.error.os.softmodule = Please select the OS to delete +message.error.ah.softmodule = Please select the Application to delete +message.error.softmodule.deleted = The selected software module is already deleted +message.cancel.action = Cancel +message.cancel.action.success = Action cancelled successfully ! +message.cancel.action.failed = Unable to cancel the action ! +message.cancel.action.confirm = Are you sure that you want to cancel this action? +message.target.alreadyAssigned = {0} Target(s) were already assigned +message.dist.alreadyAssigned = {0} Distribution Set(s) were already assigned +message.force.action = Force +message.force.action.confirm = Are you sure that you want to force this action? +message.force.action.success = Action forced successfully ! +message.distribution.no.update = distribution {0} set is already assigned to targets and cannot be changed +message.action.not.allowed = Action not allowed +message.onlyone.distribution.assigned = Only one distribution set can be assigned +message.onlyone.distribution.dropallowed = Only one distribution set can be dropped +message.error.missing.typename = Missing Type Name +message.error.missing.typenameorkey = Missing Type Name or Key +message.tag.cannot.be.assigned = Target/DS cannot be assigned to {0} +message.no.targets.assiged.fortag = No targets are assigned to tag {0} +message.error.missing.tagname = Please select tag name +message.type.delete = Please unclick the distribution type {0}, then try to delete +message.error.dist.set.type.update= Distribution Set Type is already assigned to targets and cannot be changed +message.target.ds.assign.success = Assignments saved successfully ! +message.no.directory.upload = Directory upload is not supported + +message.delete.filter.confirm = Are you sure that you want to delete custom filter? +message.delete.filter.success = Custom filter {0} deleted Successfully! +message.create.filter.success = Custom filter {0} created Successfully! +message.update.filter.success = Custom filter updated Successfully! +message.target.filter.validation = Please enter name and query +message.target.filter.duplicate = {0} already exists, please enter another value + +# action info +action.target.table.selectall = Select all (Ctrl+A) +action.target.table.clear = Clear selections + +#reused messages +soft.module.jvm =Runtime +soft.module.application =Application +soft.module.os =OS + +#Artifact upload +message.error.noFileSelected = No file selected for upload +message.error.noProvidedName = Please provide custom file name +message.error.noSwModuleSelected = Please select a software module +message.no.duplicateFiles = duplicate files selected +message.no.duplicateFile = Duplicate file selected : +message.delete.artifact = Are you sure that you want to delete artifact {0} ? +message.duplicate.filename = Duplicate file name +message.swModule.deleted = {0} Software module(s) deleted +message.error.missing.tagname = Please select tag name +message.upload.failed = Streaming Failed +message.uploadedfile.size.exceeded = File size exceeded .Allowed size {0} bytes +message.file.not.found = File not found + +upload.swModuleTable.header = Software module +upload.selectedfile.name = file selected for upload +upload.file.name = File name +upload.sha1 = SHA1 checksum +upload.md5 = MD5 checksum +upload.last.modified.date=Last modified date +upload.failed = Failed +upload.success = Success +upload.caption.add.new.swmodule = Configure New Software Module +upload.caption.delete.swmodule = Configure Delete Software Module +upload.swmodule.type = Type +upload.artifact.alreadyExists = Artifact will be overridden as the given name already exists +upload.size = Size(B) +upload.validation = Validation +upload.reason = Reason +upload.action = Action +upload.result.status = Upload status +upload.file = Upload File +upload.caption.update.swmodule = Update software module +caption.tab.details = Details +caption.tab.description = Description + +caption.delete.artifact.confirmbox = Confirm Artifact Delete Action + +#Manage distributions view +label.drop.dist.delete.area = Drop here
to delete +caption.assign.software.dist.accordion.tab = Assign Software Modules +message.software.assignment = {0} Assignment(s) done +message.dist.inuse = {0} Distribution is already assigned to target +message.software.dist.already.assigned = {0} Distribution already has software module {1} +message.software.dist.type.notallowed = {0} Software module type can not assign to Distribution {1} +message.target.assigned = {0} is assigned to {1} +message.dist.type.delete = {0} DistributionType(s) Deleted successfully. +message.sw.module.type.delete = {0} Software Module Type(s) deleted successfully. +message.dist.type.discard.success = All Distribution Types are discarded successfully ! +message.dist.discard.success = All Distributions are discarded successfully ! +message.assign.discard.success = All assignments are discarded successfully ! + +# Login view +notification.login.title=Welcome to Bosch IoT Software Provisioning. +notification.login.description=Please login with your Bosch Identity Management credentials. +notification.login.failed.title=Login failed! +notification.login.failed.description=Login with the given credentials failed. +notification.login.failed.credentialsexpired.title=Passwort Abgelaufen! +notification.login.failed.credentialsexpired.description=Passwort ist entweder abgelaufen or muss initial gesetzt werden, bitte im Benutzer Verwaltungssystem �ndern. +label.login.tenant=Tenant +label.login.username=Username +label.login.password=Password +button.login.signin=Sign in +checkbox.login.rememberme=Remember me + +# Links +link.documentation.name=Dokumentation +link.demo.name=Demo +link.requestaccount.name=Beantragung eines Accounts +link.support.name=Support +link.usermanagement.name=Benutzerverwaltung + + +# System Configuration View +notification.configuration.save=Ver�nderungen gespeichert +configuration.defaultdistributionset.title=Distribution Set Typ +configuration.defaultdistributionset.select.label=Wahl des default Distribution Set typs: +configuration.savebutton.tooltip=Konfigurationen speichern +configuration.cancellbutton.tooltip=Konfigurationen zur�cksetzen +configuration.authentication.title=Authentifikationseinstellungen +#Calendar +calendar.year=Jahr +calendar.years=Jahre +calendar.month=Monat +calendar.months=Monate +calendar.day=Tag +calendar.days=Tage +calendar.hour=Stunde +calendar.hours=Stunden +calendar.minute=Minute +calendar.minutes=Minuten +calendar.second=Sekunde +calendar.seconds=Sekunden + +header.name = Name +header.vendor = Vendor +header.version = Version +header.description = Description +header.createdBy = Created By +header.createdDate = Created Date +header.modifiedBy = Modified By +header.modifiedDate = Modified Date + +distribution.details.header = Distribution set +target.details.header = Target +header.caption.mandatory = Mandatory +header.caption.typename = SoftwareModuleType +header.caption.softwaremodule = SoftwareModule +header.caption.upload.details = Upload details +combo.type.tag.name = Type tag name + +label.yes = Yes +label.no =No + + +#Menu +menu.title = Software Provisioning diff --git a/hawkbit-ui/src/main/resources/messages_en.properties b/hawkbit-ui/src/main/resources/messages_en.properties new file mode 100755 index 000000000..678f2cf82 --- /dev/null +++ b/hawkbit-ui/src/main/resources/messages_en.properties @@ -0,0 +1,412 @@ +# +# Copyright (c) 2015 Bosch Software Innovations GmbH and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# + +######################################################################################### +# This is the messages_en.properties file +######################################################################################### + +# Button names prefix with - button +button.save = Save +button.delete = Delete +button.discard.all = Discard All +button.delete.all = Delete All +button.assign.all = Save Assign +button.actions = You have actions +button.no.actions = No actions +button.ok = OK +button.cancel = Cancel +button.upload.file = Upload File +bulk.targets.upload = Please upload csv file. +bulkupload.ds.name = DS Name + +# Headers prefix with - header +header.target.table=Targets +header.dist.table=Distributions +header.filter.tag=Filter by Tag +header.target.filter.tag=Filters +header.first.assignment.table = Targets +header.second.assignment.table = Distributions +header.dist.first.assignment.table = Distributions +header.dist.second.assignment.table = Software Modules +header.third.assignment.table = Discard +header.one.deletedist.table = Distribution Name +header.second.deletedist.table = Discard Changes +header.first.deletetarget.table = Target Name +header.second.deletetarget.table = Discard Changes +header.first.deleteswmodule.table = Delete software +header.first.delete.dist.type.table = DistributionSetType +header.second.delete.dist.type.table = Discard +header.first.delete.swmodule.type.table = Software Module Type +header.second.delete.swmodule.type.table = Discard +header.dist.twintable.selected=Selected +header.dist.twintable.available=Available +header.target.installed = Installed +header.target.assigned = Assigned + +# Captions prefix with - caption +caption.action.history = Action history for {0} +caption.error = Error +caption.new.softwaremodule.application = Configure New Application +caption.new.softwaremodule.jvm = Configure New Runtime +caption.new.softwaremodule.os = Configure New OS +caption.filter.simple = Simple Filter +caption.filter.custom = Custom Filter + +caption.add.softwaremodule = Configure Software Module +caption.add.new.dist = Configure New Distribution +caption.update.dist = Configure Update Distribution +caption.add.tag = Configure Tag +caption.add.type = Configure Type +caption.add.new.target = Configure New Target +caption.update.target = Configure Update Target +caption.bulk.upload.targets = Bulk Upload +caption.softwares.distdetail.tab = Modules +caption.tags.tab = Tags +caption.logs.tab = Logs +caption.attributes.tab = Attributes +caption.types.tab = Types +caption.save.window = Action Details +caption.assign.dist.accordion.tab = Assign Software Module +caption.delete.dist.accordion.tab = Delete Distributions +caption.delete.target.accordion.tab = Delete Targets +caption.delete.swmodule.accordion.tab = Delete SW Modules +caption.delete.dist.set.type.accordion.tab = Delete Distribution Set Type +caption.delete.sw.module.type.accordion.tab = Delete Software Module Type +caption.attributes = Attributes +caption.panel.dist.installed = Installed distribution set +caption.panel.dist.assigned = Assigned distribution set +caption.soft.delete.confirmbox = Confirm Software Module Delete Action +caption.cancel.action.confirmbox = Confirm action cancellation +caption.forced.datefield = Force update at time +caption.force.action.confirmbox = Confirm Force Active Action + +caption.filter.delete.confirmbox = Confirm Filter Delete Action + +# Labels prefix with - label +label.dist.details.type = Type : +label.dist.details.name = Name : +label.dist.details.version = Version : +label.dist.details.vendor = Vendor : +label.dist.details.jvm = Runtime : +label.dist.details.ah = Application : +label.dist.details.os = OS : +label.modified.date = Last modified at : +label.modified.by = Last modified by : +label.created.at = Created at : +label.created.by = Created by : +label.target.count = Targets : +label.description = Description : +label.ip = Address : +label.type = Type : +label.assigned.type = Assignment type : +label.assigned.count = {0} Assigned +label.installed.count = {0} Installed +label.mandatory.field = * Mandatory Field +label.components.drop.area = Drop here to delete +label.software.module.drop.area = Delete Software +label.create.tag = Create Tag +label.update.tag = Update Tag +label.create.type = Create Type +label.update.type = Update Type +label.singleAssign.type = Firmware (FW) +label.multiAssign.type = Software (SW) +label.choose.type = Choose Type to Update +label.choose.type.color = Choose Type Color +label.combobox.type = Select Type +label.combobox.tag = Select Tag +label.choose.tag = Choose Tag to update +label.choose.tag.color = Choose Tag Color +label.filter = Filter: +label.target.filter.count = Total Targets: +label.filter.selected = Selected: +label.filter.shown = Shown: +label.filter.status = Status, +label.filter.tags = Tags, +label.filter.text = Search Text +label.filter.dist = Distribution, +label.filter.custom = Custom +label.target.filter.truncated={0} targets has been truncated in the list due the target size limit of {1}, use filters to reduce the targets to be shown +label.active =Active +label.inactive = In-active +label.finished = Finished +label.error = Error +label.warning = Warning +label.running = Running +label.cancelled = Cancelled +label.cancelling = Canceling +label.retrieved = Retrieved +label.download = Downloading +label.target.id = Controller Id : +label.target.ip = Controller IP : +label.target.security.token = Security token : +label.filter.by.status = Filter by Status +label.target.controller.attrs = Controller attributes +label.target.lastpolldate = Last poll : +label.no.tag.assigned = NO TAG +label.tag.name = Tag name +label.configuration.auth.header = Allow targets to authenticate via a certificate authenticated by an reverse proxy +label.configuration.auth.gatewaytoken = Allow a gateway to authenticate and manage multiple targets through a gateway security token +label.configuration.auth.targettoken = Allow targets to authenticate directly with their target security token +label.unsupported.browser.ie=Sorry! current browser is not supported. Please use Internet Explorer 11 and above + +# Checkbox label prefix with - checkbox +checkbox.dist.migration.required = Required Migration Step : +checkbox.dist.required.migration.step = Required Migration Step + +# TextFields prefix with - textfield +textfield.name = Name +textfield.key = Key +textfield.version = Version +textfield.vendor = Vendor +textfield.description = Description +textfield.customfiltername = Filter name +ui.version = Powered by Bosch IoT Software Provisioning +prompt.target.id = Controller ID + + +#Tooltips prefix with - tooltip +tooltip.add.module = Add Software Module +tooltip.status.unknown = Unknown +tooltip.status.registered = Registered +tooltip.status.pending = Pending +tooltip.status.error = Error +tooltip.status.insync = In-sync +tooltip.delete.module = Select and delete Software Module +tooltip.forced.item=Forced update action +tooltip.soft.item=Soft update action which interacts with an user as a attempt update action for example +tooltip.timeforced.item=Soft update until a specific time and then the action will be forced +tooltip.check.for.mandatory=Check to make Mandatory +tooltip.artifact.icon=Show Artifact Details +tooltip.click.to.edit = Click to edit + +# Notification messages prefix with - message +message.save.success = {0} saved successfully +message.update.success = {0} updated successfully +message.delete.success = {0} deleted successfully +message.dist.installedorassigned = Target {targId} is already assigned/installed with distribution +message.dist.pending.action = Target {0} is already assigned with distribution {1} . Pending for action +message.empty.target.tags= No Tags Created +message.empty.disttype.tags = No Distribution type tags created +message.select.row = Please select a row to drag +message.error = Unknown error occured during the operation. Please contact administrator +message.dist.assigned.one = {0} is assigned to {1} +message.dist.assigned.many = {0} DistributionSets are assigned to {1} +message.dist.unassigned.one = {0} is unassigned from {1} +message.dist.unassigned.many = {0} DistributionSets are unassigned from {1} +message.target.assigned.one = {0} is assigned to {1} +message.target.assigned.many = {0} Targets are assigned to {1} +message.target.unassigned.one = {0} is unassigned from {1} +message.target.unassigned.many = {0} Targets are unassigned from {1} +message.target.assigned.pending = Some target(s) are already assigned.Pending for action +message.cannot.delete = Cannot be deleted +message.check.softwaremodule = Please provide both name and verion! +message.duplicate.softwaremodule = {0} : {1} already exists! +message.tag.delete = Please unclick the tag {0}, then try to delete +message.dist.type.check.delete = Please unclick the distribution type {0}, then try to delete +message.swmodule.type.check.delete = Please unclick the software module type {0}, then try to delete +message.targets.already.deleted = Few Target(s) are already deleted.Pending for action +message.dists.already.deleted = Few distribution(s) are already deleted.Pending for action +message.target.deleted.pending = Target(s) already deleted.Pending for action +message.dist.deleted.pending = Distribution(s) already deleted.Pending for action +message.dist.delete.success = All selected distribution sets are deleted successfully ! +message.dist.discard.success = All distributions selected for delete are discarded successfully ! +message.target.delete.success = All selected targets are deleted successfully ! +message.target.discard.success = All targets selected for delete are discarded successfully ! +message.software.discard.success = All software modules selected for delete are discarded successfully ! +message.software.type.discard.success = All software moduleTypes selected for delete are discarded successfully ! +message.assign.software.discard.success = All software moduleTypes selected for assign are discarded successfully ! +message.software.delete.success = All software modules selected for delete are deleted successfully ! +message.software.type.delete.success = All software modules types selected for delete are deleted successfully ! +message.dist.set.type.deleted.success = {0} DistributionSetType deleted successfully ! +message.new.dist.save.success = {0} - {1} saved successfully +message.dist.update.success = {0} - {1} updated successfully +message.duplicate.dist = Distribution set [{0}] or version [{1}] must be unique, entered value already exists. +message.error.view = No such view: {0} +message.accessdenied.view = No access to view: {0} +message.no.data = No data available +message.target.assignment = {0} Assignment(s) done +message.target.deleted = {0} Target(s) deleted +message.dist.deleted = {0} Distribution set(s) deleted +message.tag.update.mandatory = Please select the Tag to update +message.tag.duplicate.check = {0} already exists, please enter another value +message.type.key.duplicate.check = Distribution type with key {0} already exists, please give another value +message.type.key.swmodule.duplicate.check = Software Module type with key {0} already exists, please give another value +message.no.action.history = No action history is available for the target : {0} +message.no.available = --No messages available-- +message.no.actionupdateds.available = No other updates available for this action +message.mandatory.check = Mandatory details are missing +message.target.duplicate.check = Target [ {0} ] must be unique, entered value already exists. +message.permission.insufficient = Insufficient permissions to perform this action. +message.error.temp = The operation cannot be fulfilled due to {0}. Please contact administrator +message.dist.alreadyassigned = {0} : {1} is already assigned/installed, cannot be updated +message.dist.tag.alreadyassigned = {0} : {1} is already assigned/installed, cannot assign/un-assign to tag +message.dists.unassign.tag.alreadyassigned = Few of the DistributionSet's are already assigned to Target, those cannot be unassigned from Tag" +message.dists.assign.tag.alreadyassigned = Few of the DistributionSet's are already assigned to Target, those cannot be assigned to Tag" +message.dists.tag.assigned = {0} DistributionSet's assigned to Tag {1} +message.dists.tag.unassigned = {0} DistributionSet's un-assigned from Tag {1} +message.dist.no.operation = {0} - already assigned/installed, No operation +message.sm.delete.confirm = Are you sure that you want to delete the selected {0} software module? +message.error.os.softmodule = Please select the OS to delete +message.error.ah.softmodule = Please select the Application to delete +message.error.softmodule.deleted = The selected software module is already deleted +message.cancel.action = Cancel +message.cancel.action.success = Action cancelled successfully ! +message.cancel.action.failed = Unable to cancel the action ! +message.cancel.action.confirm = Are you sure that you want to cancel this action? +message.target.alreadyAssigned = {0} Target(s) were already assigned +message.dist.alreadyAssigned = {0} Distribution Set(s) were already assigned +message.force.action = Force +message.force.action.confirm = Are you sure that you want to force this action? +message.force.action.success = Action forced successfully ! +message.distribution.no.update = distribution {0} set is already assigned to targets and cannot be changed +message.action.not.allowed = Action not allowed +message.onlyone.distribution.assigned = Only one distribution set can be assigned +message.onlyone.distribution.dropallowed = Only one distribution set can be dropped +message.error.missing.typename = Missing Type Name +message.error.missing.typenameorkey = Missing Type Name or Key or software module type +message.tag.cannot.be.assigned = Target/DS cannot be assigned to {0} +message.no.targets.assiged.fortag = No targets are assigned to tag {0} +message.error.missing.tagname = Please select tag name +message.type.delete = Please unclick the distribution type {0}, then try to delete +message.error.dist.set.type.update= Distribution Set Type is already assigned to targets and cannot be changed +message.target.ds.assign.success = Assignments saved successfully ! +message.no.directory.upload = Directory upload is not supported + +message.delete.filter.confirm = Are you sure that you want to delete custom filter? +message.delete.filter.success = Custom filter {0} deleted Successfully! +message.create.filter.success = Custom filter {0} created Successfully! +message.update.filter.success = Custom filter updated Successfully! +message.target.filter.validation = Please enter name and query +message.target.filter.duplicate = {0} already exists, please enter another value + +# action info +action.target.table.selectall = Select all (Ctrl+A) +action.target.table.clear = Clear selections + +#reused messages +soft.module.jvm =Runtime +soft.module.application =Application +soft.module.os =OS + +#Artifact upload +message.error.noFileSelected = No file selected for upload +message.error.noProvidedName = Please provide custom file name +message.error.noSwModuleSelected = Please select a software module +message.no.duplicateFiles = duplicate files selected +message.no.duplicateFile = Duplicate file selected : +message.delete.artifact = Are you sure that you want to delete artifact {0} ? +message.duplicate.filename = Duplicate file name +message.swModule.deleted = {0} Software module(s) deleted +message.upload.failed = Streaming Failed +message.uploadedfile.size.exceeded = File size exceeded .Allowed size {0} bytes +message.file.not.found = File not found + +upload.swModuleTable.header = Software module +upload.selectedfile.name = file selected for upload +upload.file.name = File name +upload.sha1 = SHA1 checksum +upload.md5 = MD5 checksum +upload.last.modified.date=Last modified date +upload.failed = Failed +upload.success = Success +upload.caption.add.new.swmodule = Configure New Software Module +upload.caption.delete.swmodule = Configure Delete Software Module +upload.swmodule.type = Type +upload.artifact.alreadyExists = Artifact will be overridden as the given name already exists +upload.size = Size(B) +upload.validation = Validation +upload.reason = Reason +upload.action = Action +upload.result.status = Upload status +upload.file = Upload File +upload.caption.update.swmodule = Update software module +caption.tab.details = Details +caption.tab.description = Description + +caption.delete.artifact.confirmbox = Confirm Artifact Delete Action + + +#Manage distributions view +label.drop.dist.delete.area = Drop here
to delete +caption.assign.software.dist.accordion.tab = Assign Software Modules +message.software.assignment = {0} Software Module(s) Assignment(s) done +message.dist.inuse = {0} Distribution is already assigned to target +message.software.dist.already.assigned = {0} Distribution already has software module {1} +message.software.dist.type.notallowed = {0} Software module type can not assign to Distribution {1} +message.target.assigned = {0} is assigned to {1} +message.dist.type.delete = {0} DistributionType(s) Deleted successfully. +message.sw.module.type.delete = {0} Software Module Type(s) deleted successfully. +message.dist.type.discard.success = All Distribution Types are discarded successfully ! +message.dist.discard.success = All Distributions are discarded successfully ! +message.assign.discard.success = All assignments are discarded successfully ! + +# Login view +notification.login.title=Welcome to Bosch IoT Software Provisioning. +notification.login.description=Please login with your Bosch Identity Management credentials. +notification.login.failed.title=Login failed! +notification.login.failed.description=Login with the given credentials failed. +notification.login.failed.credentialsexpired.title=Password Expired! +notification.login.failed.credentialsexpired.description=Password has been expired or needs to be set initially, please visit the User Management to change or set your password. +label.login.tenant=Tenant +label.login.username=Username +label.login.password=Password +button.login.signin=Sign in +checkbox.login.rememberme=Remember me + +# Links +link.documentation.name=Documentation +link.demo.name=Demo +link.requestaccount.name=Request Account +link.support.name=Support +link.usermanagement.name=User Management + +# System Configuration View +notification.configuration.save=Saved changes +configuration.defaultdistributionset.title=Distribution Set Type +configuration.defaultdistributionset.select.label=Select the default Distribution Set type: +configuration.savebutton.tooltip=Save Configurations +configuration.cancellbutton.tooltip=Cancel Configurations +configuration.authentication.title=Authentication Configuration + +#Calendar +calendar.year=year +calendar.years=years +calendar.month=month +calendar.months=months +calendar.day=day +calendar.days=days +calendar.hour=hour +calendar.hours=hours +calendar.minute=minute +calendar.minutes=minutes +calendar.second=second +calendar.seconds=seconds + +header.name = Name +header.vendor = Vendor +header.version = Version +header.description = Description +header.createdBy = Created By +header.createdDate = Created Date +header.modifiedBy = Modified By +header.modifiedDate = Modified Date + +distribution.details.header = Distribution set +target.details.header = Target +header.caption.mandatory = Mandatory +header.caption.typename = SoftwareModuleType +header.caption.softwaremodule = SoftwareModule +header.caption.upload.details = Upload details +combo.type.tag.name = Type tag name + +label.yes = Yes +label.no =No + +#Menu +menu.title = Software Provisioning diff --git a/hawkbit-ui/src/test/java/org/eclipse/hawkbit/push/SpringSecurityAtmosphereInterceptorTest.java b/hawkbit-ui/src/test/java/org/eclipse/hawkbit/push/SpringSecurityAtmosphereInterceptorTest.java new file mode 100755 index 000000000..84b48a004 --- /dev/null +++ b/hawkbit-ui/src/test/java/org/eclipse/hawkbit/push/SpringSecurityAtmosphereInterceptorTest.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.push; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import javax.servlet.http.HttpSession; + +import org.atmosphere.cpr.AtmosphereRequest; +import org.atmosphere.cpr.AtmosphereResource; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; + +@RunWith(MockitoJUnitRunner.class) +public class SpringSecurityAtmosphereInterceptorTest { + + @Mock + private AtmosphereResource atmosphereResourceMock; + @Mock + private AtmosphereRequest atmosphereRequestMock; + @Mock + private SecurityContext sessionSecurityContextMock; + @Mock + private HttpSession httpSessionMock; + + private final SpringSecurityAtmosphereInterceptor underTest = new SpringSecurityAtmosphereInterceptor(); + + @After + public void after() { + SecurityContextHolder.clearContext(); + } + + @Test + public void inspectRetrievesSetsSecurityContextFromRequestToThreadLocal() { + + when(atmosphereResourceMock.getRequest()).thenReturn(atmosphereRequestMock); + when(atmosphereRequestMock.getSession()).thenReturn(httpSessionMock); + when(httpSessionMock.getAttribute(Mockito.eq(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY))) + .thenReturn(sessionSecurityContextMock); + underTest.inspect(atmosphereResourceMock); + // verify + assertThat(SecurityContextHolder.getContext()).isEqualTo(sessionSecurityContextMock); + } + + @Test + public void afterAtmosphereRequestSecurityContextGetsCleared() { + SecurityContextHolder.setContext(sessionSecurityContextMock); + + underTest.postInspect(atmosphereResourceMock); + + assertThat(SecurityContextHolder.getContext()).isNotEqualTo(sessionSecurityContextMock); + } +} diff --git a/hawkbit-ui/src/test/java/org/eclipse/hawkbit/ui/utils/NamingThreadFactoryTest.java b/hawkbit-ui/src/test/java/org/eclipse/hawkbit/ui/utils/NamingThreadFactoryTest.java new file mode 100755 index 000000000..d351f976d --- /dev/null +++ b/hawkbit-ui/src/test/java/org/eclipse/hawkbit/ui/utils/NamingThreadFactoryTest.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.utils; + +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.concurrent.ThreadFactory; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class NamingThreadFactoryTest { + @Mock + private final Runnable runnableMock = mock(Runnable.class); + + @Test + public void setsNameForThreads() { + final String knownName = "knownName"; + final ThreadFactory threadFactory = new NamingThreadFactory(knownName); + final Thread newThread1 = threadFactory.newThread(runnableMock); + final Thread newThread2 = threadFactory.newThread(runnableMock); + + assertThat(newThread1.getName()).isEqualTo(NamingThreadFactory.SP_PREFIX + knownName); + assertThat(newThread2.getName()).isEqualTo(NamingThreadFactory.SP_PREFIX + knownName); + } + + @Test + public void setsFormatedNameForThreads() { + final String nameFormat = "knownName-%d"; + final String knownName1 = "knownName-0"; + final String knownName2 = "knownName-1"; + final ThreadFactory threadFactory = new NamingThreadFactory(nameFormat); + final Thread newThread1 = threadFactory.newThread(runnableMock); + final Thread newThread2 = threadFactory.newThread(runnableMock); + + assertThat(newThread1.getName()).isEqualTo(NamingThreadFactory.SP_PREFIX + knownName1); + assertThat(newThread2.getName()).isEqualTo(NamingThreadFactory.SP_PREFIX + knownName2); + } + + @Test + public void setsRunnableForThreads() { + final String knownName = "knownName"; + final ThreadFactory threadFactory = new NamingThreadFactory(knownName); + final Thread newThread1 = threadFactory.newThread(runnableMock); + final Thread newThread2 = threadFactory.newThread(runnableMock); + + newThread1.run(); + newThread2.run(); + + verify(runnableMock, times(2)).run(); + } +} diff --git a/hawkbit-ui/src/test/java/org/eclipse/hawkbit/ui/utils/SPUIComponentProviderTest.java b/hawkbit-ui/src/test/java/org/eclipse/hawkbit/ui/utils/SPUIComponentProviderTest.java new file mode 100755 index 000000000..1f4798ad5 --- /dev/null +++ b/hawkbit-ui/src/test/java/org/eclipse/hawkbit/ui/utils/SPUIComponentProviderTest.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.utils; + +import static org.fest.assertions.api.Assertions.assertThat; + +import org.eclipse.hawkbit.ui.components.SPUIButton; +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleSmallNoBorderUH; +import org.junit.Test; + +import com.vaadin.shared.ui.label.ContentMode; +import com.vaadin.ui.Button; +import com.vaadin.ui.Label; + +/** + * Unit Test block for UI Component provider. Dynamic Factory Testing. + * + * + * + */ +public class SPUIComponentProviderTest { + /** + * Test case for check button factory. + * + * @throws Exception + */ + @Test + public void checkButtonFactory() throws Exception { + + // Checking Dyanmic Factory + Button placeHolderButton = null; + placeHolderButton = SPUIComponentProvider.getButton("", "Test", "Test", + SPUIButtonDefinitions.SP_BUTTON_STATUS_STYLE, true, null, SPUIButtonStyleSmallNoBorderUH.class); + assertThat(placeHolderButton).isInstanceOf(SPUIButton.class); + assertThat(placeHolderButton.getCaption()).isEqualTo("Test"); + assertThat(placeHolderButton.getStyleName()).isEqualTo(SPUIButtonDefinitions.SP_BUTTON_STATUS_STYLE); + } + + /** + * Test case for check Label factory. + * + * @throws Exception + */ + @Test + public void checkLabelFactory() throws Exception { + + Label placeHolderLabel = null; + placeHolderLabel = SPUIComponentProvider.getLabel("TestTable", SPUILabelDefinitions.SP_WIDGET_CAPTION); + assertThat(placeHolderLabel).isInstanceOf(Label.class); + assertThat(placeHolderLabel.getValue()).isEqualTo("TestTable"); + assertThat(placeHolderLabel.getContentMode()).isNotEqualTo(ContentMode.HTML); + placeHolderLabel = SPUIComponentProvider.getLabel("TestMSG", SPUILabelDefinitions.SP_LABEL_MESSAGE); + assertThat(placeHolderLabel.getValue()).isEqualTo("TestMSG"); + assertThat(placeHolderLabel.getContentMode()).isEqualTo(ContentMode.HTML); + } + +} diff --git a/pom.xml b/pom.xml new file mode 100755 index 000000000..aa4807446 --- /dev/null +++ b/pom.xml @@ -0,0 +1,624 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 1.2.7.RELEASE + + + org.eclipse.hawkbit + hawkbit-parent + 0.1.0-SNAPSHOT + pom + hawkBit :: Parent + + + hawkbit-dmf-api + hawkbit-rest-api + hawkbit-core + hawkbit-security-core + hawkbit-repository + hawkbit-dmf-amqp + hawkbit-security-integration + hawkbit-http-security + hawkbit-rest-resource + hawkbit-ui + hawkbit-artifact-repository-mongo + hawkbit-autoconfigure + hawkbit-cache-redis + hawkbit-test-report + examples + + + + + scm:git:https://github.com/bsinno/hawkbit-update-server.git + scm:git:https://github.com/bsinno/hawkbit-update-server.git + https://github.com/bsinno/hawkbit-update-server.git + + + + + sp-snapshots + false + https://repo.bosch-si.com/content/repositories/sp-snapshots + + + sp-releases + https://repo.bosch-si.com/content/repositories/sp-releases + + + + + + vaadin-addons + http://maven.vaadin.com/vaadin-addons + + + + + 1.2.7.RELEASE + + + 1.4.186 + 2.5.0 + 5.2.2.Final + Fowler-SR1 + 1.2.0.RELEASE + 3.0.2 + + + + 1.0.0 + 0.0.6.RELEASE + 7.5.7 + ${vaadin.version} + + + + 1.4 + 2.0M10 + 1.8 + 1.4.15 + 2.6.0 + 1.5.4 + 1.0.2 + 0.9.1 + 3.4 + 1.8.5 + + + https://sonar.eu-gb.mybluemix.net + bsinno/hawkbit-update-server + jacoco + + 0.7.2.201409121644 + + 1.4 + + reuseReports + + ${project.basedir}/../target/ + + jacoco-ut.exec + + ${jacoco.outputDir}/${jacoco.out.ut.file} + + jacoco-it.exec + + ${jacoco.outputDir}/${jacoco.out.it.file} + + + + + + + com.mycila + license-maven-plugin + 2.11 + +
LICENCE_HEADER_TEMPLATE.txt
+ + **/README + 3rd-dependencies/** + **/.git* + .git* + **/*.sql + **/*.sql + eclipse_codeformatter.xml + + + JAVADOC_STYLE + +
+
+ + org.codehaus.mojo + versions-maven-plugin + + + maven-scm-plugin + 1.9.4 + + ${project.version} + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + prepare-ut-agent + process-test-classes + + prepare-agent + + + ${sonar.jacoco.reportPath} + jacoco.agent.ut.arg + true + + + + prepare-it-agent + pre-integration-test + + prepare-agent + + + ${sonar.jacoco.itReportPath} + jacoco.agent.it.arg + true + + + + +
+ + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.apache.maven.plugins + maven-dependency-plugin + [2.8,) + + copy-dependencies + + + + + + + + + com.vaadin + vaadin-maven-plugin + [7.3.0,) + + resources + update-widgetset + + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.17 + + true + 1 + + ${jacoco.agent.ut.arg} + -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" + + + + listener + ru.yandex.qatools.allure.junit.AllureRunListener + + + + **/*Tests.java + **/*Test.java + **/*IT.java + + + **/Abstract*.java + + + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + true + 3 + -Xmx1024m ${jacoco.agent.ut.arg} + -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" + + + listener + ru.yandex.qatools.allure.junit.AllureRunListener + + + + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + + + integration-test + integration-test + + integration-test + + + + + + org.apache.maven.plugins + maven-site-plugin + + true + true + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.maven.version} + + + + +
+ + + + + + org.slf4j + slf4j-api + 1.7.7 + + + + com.vaadin + vaadin-spring-boot + ${vaadin.spring.version} + + + org.vaadin.spring.addons + vaadin-spring-addon-eventbus + ${vaadin.spring.addon.version} + + + org.vaadin.spring.extensions + vaadin-spring-ext-security + ${vaadin.spring.addon.version} + + + com.vaadin + vaadin-server + ${vaadin.version} + + + com.vaadin + vaadin-push + ${vaadin.version} + + + + com.vaadin + vaadin-client-compiler + ${vaadin.version} + provided + + + org.eclipse.jetty + jetty-servlets + + + org.eclipse.jetty + jetty-annotations + + + org.eclipse.jetty + jetty-util + + + + + com.vaadin + vaadin-themes + ${vaadin.version} + + + org.vaadin.addons.lazyquerycontainer + vaadin-lazyquerycontainer + 7.4.0.1 + + + org.vaadin.addons + flexibleoptiongroup + 2.2.0 + + + org.vaadin.addons + tokenfield + 7.0.1 + + + + + javax.el + javax.el-api + 2.2.4 + + + net.sf.corn + corn-cps + 1.1.7 + + + net._01001111 + jlorem + 1.1 + + + + io.springfox + springfox-swagger2 + 2.0.3 + + + io.swagger + swagger-annotations + 1.5.0 + + + + org.springframework.boot + spring-boot-starter-actuator + ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-log4j2 + ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-web + ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter + ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-websocket + ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-cloud-connectors + ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-aop + ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-data-mongodb + ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-tomcat + ${spring.boot.version} + provided + + + org.springframework.boot + spring-boot-starter-data-jpa + ${spring.boot.version} + + + org.hibernate + hibernate-entitymanager + + + org.springframework.boot + spring-boot-starter-logging + + + + + + com.fasterxml + classmate + 1.3.0 + + + org.eclipse.persistence + org.eclipse.persistence.jpa + ${eclipselink.version} + + + + cz.jirutka.rsql + rsql-parser + 2.0.0 + + + + com.jayway.jsonpath + json-path + ${json-path.version} + + + com.googlecode.json-simple + json-simple + 1.1.1 + + + junit + junit + + + + + + xml-apis + xml-apis + 1.4.01 + + + + org.springframework.boot + spring-boot-starter-test + ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-logging + + + + + com.vaadin + vaadin-testbench + 4.0.3 + test + + + org.json + json + 20141113 + test + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + 1.50.0 + test + + + org.easytesting + fest-assert + ${fest-assert.version} + test + + + org.easytesting + fest-assert-core + ${org.easytesting.version} + test + + + ru.yandex.qatools.allure + allure-junit-adaptor + ${allure.version} + test + + + com.sun.jersey + jersey-client + 1.18.1 + test + + + pl.pragmatists + JUnitParams + ${pl.pragmatists.version} + test + + + com.jayway.jsonpath + json-path-assert + 0.9.1 + test + + + org.mariadb.jdbc + mariadb-java-client + 1.2.3 + test + + + +
diff --git a/sonarCircleCi.sh b/sonarCircleCi.sh new file mode 100755 index 000000000..5167d1724 --- /dev/null +++ b/sonarCircleCi.sh @@ -0,0 +1,30 @@ +# +# Copyright (c) 2015 Bosch Software Innovations GmbH and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# + +echo $CI_PULL_REQUEST pull request +# regular sonar on master +if [ "$CIRCLE_BRANCH" = "master" ]; then + mvn verify license:check sonar:sonar -Dsonar.login=$SONAR_SERVER_USER -Dsonar.password=$SONAR_SERVER_PASSWD +# preview in case of pull request +else + if [ -n "$CI_PULL_REQUEST" ]; then + mvn org.jacoco:jacoco-maven-plugin:prepare-agent verify license:check sonar:sonar -B -e -V \ + -Dmaven.test.failure.ignore=true \ + -Dclirr=true \ + -Dsonar.analysis.mode=issues \ + -Dsonar.github.pullRequest=`echo $CI_PULL_REQUEST| awk -F'/' '{print $7}'` \ + -Dsonar.github.login=$SONAR_GITHUB_LOGIN \ + -Dsonar.github.oauth=$SONAR_GITHUB_OAUTH \ + -Dsonar.login=$SONAR_SERVER_USER \ + -Dsonar.password=$SONAR_SERVER_PASSWD + else + mvn verify license:check + fi +fi +# but noting in case of other branches