diff --git a/.gitignore b/.gitignore index 1b1a367..ee01dda 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,6 @@ gradle-app.setting # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 # gradle/wrapper/gradle-wrapper.properties + +# IntelliJ +*.iml \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b618db2..2e3639a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -61,3 +61,4 @@ code_quality: - build/reports/cpd/cpdCheck.xml #create a report on the quality of the code expose_as: 'Code Quality Report' + allow_failure: true diff --git a/backend.iml b/backend.iml deleted file mode 100644 index 1eae0df..0000000 --- a/backend.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle index d4f0bd4..8ef658c 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'org.springframework.boot:spring-boot-starter-websocket' + implementation 'org.springframework:spring-websocket:5.2.4.RELEASE' implementation 'io.jsonwebtoken:jjwt:0.9.1' implementation 'org.springframework.security:spring-security-web' implementation 'org.postgresql:postgresql' @@ -27,6 +29,7 @@ dependencies { compile 'io.springfox:springfox-swagger2:2.9.2' compile 'io.springfox:springfox-swagger-ui:2.9.2' compile 'org.springframework.boot:spring-boot-configuration-processor' + testCompile 'org.springframework.boot:spring-boot-starter-webflux' implementation('org.springframework.boot:spring-boot-starter-web') { exclude group: 'org.springframework.boot', module: 'spring-boot-starter-json' diff --git a/gradle.yml b/gradle.yml new file mode 100644 index 0000000..51fefa4 --- /dev/null +++ b/gradle.yml @@ -0,0 +1,39 @@ +# vim: set ts=2 sw=2 et tw=80: +image: gradle:jdk13 + +stages: + - build + - test + - deploy + +smarthut_build: + stage: build + script: + - gradle assemble + artifacts: + paths: + - build/libs/*.jar + expire_in: 1 week + +smarthut_test: + stage: test + script: + - gradle check + +smarthut_deploy: + stage: deploy + image: docker:latest + services: + - docker:dind + variables: + DOCKER_DRIVER: overlay + before_script: + - docker version + - docker info + - docker login -u smarthutsm -p $CI_DOCKER_PASS + script: + - "docker build -t smarthutsm/smarthut:${CI_COMMIT_BRANCH} --pull ." + - "docker push smarthutsm/smarthut:${CI_COMMIT_BRANCH}" + after_script: + - docker logout + diff --git a/socket_test.html b/socket_test.html new file mode 100644 index 0000000..687388b --- /dev/null +++ b/socket_test.html @@ -0,0 +1,40 @@ + + + + + + + +
+

Waiting for authentication...

+
+ + + diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplication.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplication.java index 242f03f..57f7b42 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplication.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplication.java @@ -3,8 +3,10 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling @EnableJpaRepositories("ch.usi.inf.sa4.sanmarinoes.smarthut.models") public class SmarthutApplication { public static void main(String[] args) { diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java index 3a2ab37..69c4fc9 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java @@ -20,9 +20,10 @@ public class GsonConfig { return converter; } - private Gson gson() { + public static Gson gson() { final GsonBuilder builder = new GsonBuilder(); builder.registerTypeAdapter(Json.class, new SpringfoxJsonToGsonAdapter()); + builder.addSerializationExclusionStrategy(new AnnotationExclusionStrategy()); return builder.create(); } } @@ -34,3 +35,16 @@ class SpringfoxJsonToGsonAdapter implements JsonSerializer { return JsonParser.parseString(json.value()); } } + +/** GSON exclusion strategy to exclude attributes with @GsonExclude */ +class AnnotationExclusionStrategy implements ExclusionStrategy { + @Override + public boolean shouldSkipField(FieldAttributes f) { + return f.getAnnotation(GsonExclude.class) != null; + } + + @Override + public boolean shouldSkipClass(Class clazz) { + return false; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonExclude.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonExclude.java new file mode 100644 index 0000000..1be5551 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonExclude.java @@ -0,0 +1,10 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface GsonExclude {} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java index 853083b..e0cbb6a 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java @@ -20,7 +20,7 @@ public class JWTRequestFilter extends OncePerRequestFilter { @Autowired private JWTUserDetailsService jwtUserDetailsService; - @Autowired private JWTTokenUtil jwtTokenUtil; + @Autowired private JWTTokenUtils jwtTokenUtils; @Override protected void doFilterInternal( @@ -30,13 +30,11 @@ public class JWTRequestFilter extends OncePerRequestFilter { String username = null; String jwtToken = null; - // JWT Token is in th - // e form "Bearer token". Remove Bearer word and get - // only the Token + // JWT Token is in the form "Bearer token". Remove Bearer word and get only the Token if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { jwtToken = requestTokenHeader.substring(7); try { - username = jwtTokenUtil.getUsernameFromToken(jwtToken); + username = jwtTokenUtils.getUsernameFromToken(jwtToken); } catch (IllegalArgumentException e) { System.out.println("Unable to get JWT Token"); } catch (ExpiredJwtException e) { @@ -44,14 +42,15 @@ public class JWTRequestFilter extends OncePerRequestFilter { } } else { logger.warn("JWT Token does not begin with Bearer String"); - } // Once we get the token validate it. + } + + // Once we get the token validate it. if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername( username); // if token is valid configure Spring Security to manually - // set - // authentication - if (jwtTokenUtil.validateToken(jwtToken, userDetails)) { + // set authentication + if (jwtTokenUtils.validateToken(jwtToken, userDetails)) { UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtil.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtil.java deleted file mode 100644 index 40d369f..0000000 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtil.java +++ /dev/null @@ -1,72 +0,0 @@ -package ch.usi.inf.sa4.sanmarinoes.smarthut.config; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; - -@Component -public class JWTTokenUtil { - public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60; - - @Value("${jwt.secret}") - private String secret; - - // retrieve username from jwt token - public String getUsernameFromToken(String token) { - return getClaimFromToken(token, Claims::getSubject); - } - - // retrieve expiration date from jwt token - public Date getExpirationDateFromToken(String token) { - return getClaimFromToken(token, Claims::getExpiration); - } - - public T getClaimFromToken(String token, Function claimsResolver) { - final Claims claims = getAllClaimsFromToken(token); - return claimsResolver.apply(claims); - } - - // for retrieveing any information from token we will need the secret key - private Claims getAllClaimsFromToken(String token) { - return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); - } // check if the token has expired - - private Boolean isTokenExpired(String token) { - final Date expiration = getExpirationDateFromToken(token); - return expiration.before(new Date()); - } // generate token for user - - public String generateToken(UserDetails userDetails) { - Map claims = new HashMap<>(); - return doGenerateToken(claims, userDetails.getUsername()); - } - - // while creating the token - - // 1. Define claims of the token, like Issuer, Expiration, Subject, and the ID - // 2. Sign the JWT using the HS512 algorithm and secret key. - // 3. According to JWS Compact - // Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1) - // compaction of the JWT to a URL-safe string - private String doGenerateToken(Map claims, String subject) { - return Jwts.builder() - .setClaims(claims) - .setSubject(subject) - .setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000)) - .signWith(SignatureAlgorithm.HS512, secret) - .compact(); - } - - // validate token - public Boolean validateToken(String token, UserDetails userDetails) { - final String username = getUsernameFromToken(token); - return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); - } -} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtils.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtils.java new file mode 100644 index 0000000..f6943a8 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtils.java @@ -0,0 +1,84 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.config; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import java.util.Date; +import java.util.HashMap; +import java.util.function.Function; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +/** A utility class to handle JWTs */ +@Component +public class JWTTokenUtils { + /** The duration in seconds of the validity of a single token */ + private static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60; + + /** The secret key used to encrypt all JWTs */ + @Value("${jwt.secret}") + private String secret; + + /** + * Retrieves the claimed username from a given token + * + * @param token the token to inspect + * @return the username + */ + public String getUsernameFromToken(String token) { + return getClaimFromToken(token, Claims::getSubject); + } + + /** + * Returns whether the token given is expired or not + * + * @param token the given token + * @return true if expired, false if not + */ + public Boolean isTokenExpired(String token) { + final Date expiration = getClaimFromToken(token, Claims::getExpiration); + return expiration.before(new Date()); + } + + /** + * Creates a new JWT for a given user. While creating the token - 1. Define claims of the token, + * like Issuer, Expiration, Subject, and the ID 2. Sign the JWT using the HS512 algorithm and + * secret key. 3. According to JWS Compact Serialization + * (https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1) compaction of + * the JWT to a URL-safe string + * + * @param user the user to which create a JWT + * @return the newly generated token + */ + public String generateToken(UserDetails user) { + return Jwts.builder() + .setClaims(new HashMap<>()) + .setSubject(user.getUsername()) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000)) + .signWith(SignatureAlgorithm.HS512, secret) + .compact(); + } + + /** + * Validates the token given against matching userDetails + * + * @param token the token given + * @param userDetails user details to validate against + * @return true if valid, false if not + */ + public Boolean validateToken(String token, UserDetails userDetails) { + final String username = getUsernameFromToken(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + private T getClaimFromToken(String token, Function claimsResolver) { + final Claims claims = getAllClaimsFromToken(token); + return claimsResolver.apply(claims); + } + + private Claims getAllClaimsFromToken(String token) { + return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/SpringFoxConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/SpringFoxConfig.java index 4011592..2fdab4e 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/SpringFoxConfig.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/SpringFoxConfig.java @@ -1,6 +1,5 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.config; -import static springfox.documentation.builders.PathSelectors.regex; import java.util.List; import java.util.function.Predicate; @@ -10,10 +9,9 @@ import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; -import springfox.documentation.service.ApiInfo; -import springfox.documentation.service.ApiKey; -import springfox.documentation.service.SecurityScheme; +import springfox.documentation.service.*; import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.contexts.SecurityContext; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @@ -39,7 +37,8 @@ public class SpringFoxConfig { .paths(paths()::test) .build() .apiInfo(apiInfo()) - .securitySchemes(securitySchemes()); + .securitySchemes(securitySchemes()) + .securityContexts(List.of(securityContext())); } /** @@ -51,14 +50,32 @@ public class SpringFoxConfig { return List.of(new ApiKey("Bearer", "Authorization", "header")); } - /** - * Return a Java functional API predicate for regex matches - * - * @param regex the regex to match on - * @return a Java functional API predicate - */ - private Predicate regexPredicate(final String regex) { - return regex(regex)::apply; + private SecurityContext securityContext() { + return SecurityContext.builder() + .securityReferences(defaultAuth()) + .forPaths(authenticatedPaths()::test) + .build(); + } + + private List defaultAuth() { + final AuthorizationScope authorizationScope = + new AuthorizationScope("global", "accessEverything"); + return List.of( + new SecurityReference("Bearer", new AuthorizationScope[] {authorizationScope})); + } + + private Predicate authenticatedPaths() { + return ((Predicate) PathSelectors.regex("/auth/update")::apply) + .or(PathSelectors.regex("/room.*")::apply) + .or(PathSelectors.regex("/device.*")::apply) + .or(PathSelectors.regex("/buttonDimmer.*")::apply) + .or(PathSelectors.regex("/dimmableLight.*")::apply) + .or(PathSelectors.regex("/knobDimmer.*")::apply) + .or(PathSelectors.regex("/regularLight.*")::apply) + .or(PathSelectors.regex("/sensor.*")::apply) + .or(PathSelectors.regex("/smartPlug.*")::apply) + .or(PathSelectors.regex("/switch.*")::apply) + .or(PathSelectors.regex("/motionSensor.*")::apply); } /** diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java index 253998d..ec116c3 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java @@ -51,6 +51,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { // dont authenticate this particular request .authorizeRequests() .antMatchers( + "/sensor-socket", "/auth/login", "/swagger-ui.html", "/register", diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java index 1a1e266..3160e1c 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java @@ -1,6 +1,6 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; -import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtil; +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtils; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.JWTRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.JWTResponse; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserUpdateRequest; @@ -26,7 +26,7 @@ public class AuthenticationController { private final UserRepository userRepository; - private final JWTTokenUtil jwtTokenUtil; + private final JWTTokenUtils jwtTokenUtils; private final JWTUserDetailsService userDetailsService; @@ -35,11 +35,11 @@ public class AuthenticationController { public AuthenticationController( AuthenticationManager authenticationManager, UserRepository userRepository, - JWTTokenUtil jwtTokenUtil, + JWTTokenUtils jwtTokenUtils, JWTUserDetailsService userDetailsService) { this.authenticationManager = authenticationManager; this.userRepository = userRepository; - this.jwtTokenUtil = jwtTokenUtil; + this.jwtTokenUtils = jwtTokenUtils; this.userDetailsService = userDetailsService; } @@ -68,7 +68,7 @@ public class AuthenticationController { authenticationRequest.getUsernameOrEmail()); } - final String token = jwtTokenUtil.generateToken(userDetails); + final String token = jwtTokenUtils.generateToken(userDetails); return new JWTResponse(token); } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ButtonDimmerController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ButtonDimmerController.java index 3d59036..061c6b9 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ButtonDimmerController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ButtonDimmerController.java @@ -2,11 +2,12 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; -import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.ButtonDimmerSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.ButtonDimmerDimRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveReguest; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ButtonDimmer; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ButtonDimmerRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import java.util.List; +import java.util.Set; import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -15,37 +16,77 @@ import org.springframework.web.bind.annotation.*; @RestController @EnableAutoConfiguration @RequestMapping("/buttonDimmer") -public class ButtonDimmerController { - @Autowired private ButtonDimmerRepository buttonDimmerService; +public class ButtonDimmerController + extends InputDeviceConnectionController { + private ButtonDimmerRepository buttonDimmerRepository; + private DimmableLightRepository dimmableLightRepository; + + @Autowired + protected ButtonDimmerController( + ButtonDimmerRepository inputRepository, DimmableLightRepository outputRepository) { + super( + inputRepository, + outputRepository, + DimmableLight.BUTTON_DIMMER_DIMMABLE_LIGHT_CONNECTOR); + this.buttonDimmerRepository = inputRepository; + this.dimmableLightRepository = outputRepository; + } @GetMapping public List findAll() { - return toList(buttonDimmerService.findAll()); + return toList(buttonDimmerRepository.findAll()); } @GetMapping("/{id}") public ButtonDimmer findById(@PathVariable("id") long id) throws NotFoundException { - return buttonDimmerService.findById(id).orElseThrow(NotFoundException::new); + return buttonDimmerRepository.findById(id).orElseThrow(NotFoundException::new); } @PostMapping - public ButtonDimmer create(@Valid @RequestBody final ButtonDimmerSaveRequest bd) { + public ButtonDimmer create(@Valid @RequestBody final GenericDeviceSaveReguest bd) { ButtonDimmer newBD = new ButtonDimmer(); - newBD.setLights(bd.getLights()); - newBD.setId(bd.getId()); newBD.setName(bd.getName()); newBD.setRoomId(bd.getRoomId()); - return buttonDimmerService.save(newBD); + return buttonDimmerRepository.save(newBD); } - @PutMapping - public ButtonDimmer update(@Valid @RequestBody ButtonDimmerSaveRequest bd) { - return this.create(bd); + @PutMapping("/dim") + public Set dim(@Valid @RequestBody final ButtonDimmerDimRequest bd) + throws NotFoundException { + final ButtonDimmer buttonDimmer = + buttonDimmerRepository.findById(bd.getId()).orElseThrow(NotFoundException::new); + + switch (bd.getDimType()) { + case UP: + buttonDimmer.increaseIntensity(); + break; + case DOWN: + buttonDimmer.decreaseIntensity(); + break; + } + + dimmableLightRepository.saveAll(buttonDimmer.getOutputs()); + + return buttonDimmer.getOutputs(); + } + + @PostMapping("/{id}/lights") + public Set addLight( + @PathVariable("id") long inputId, @RequestParam("lightId") Long lightId) + throws NotFoundException { + return addOutput(inputId, lightId); + } + + @DeleteMapping("/{id}/lights") + public Set removeLight( + @PathVariable("id") long inputId, @RequestParam("lightId") Long lightId) + throws NotFoundException { + return removeOutput(inputId, lightId); } @DeleteMapping("/{id}") public void delete(@PathVariable("id") long id) { - buttonDimmerService.deleteById(id); + buttonDimmerRepository.deleteById(id); } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DeviceController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DeviceController.java new file mode 100644 index 0000000..2d8dd99 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DeviceController.java @@ -0,0 +1,48 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.DeviceSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.BadDataException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Device; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DeviceRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RoomRepository; +import java.security.Principal; +import java.util.List; +import javax.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.web.bind.annotation.*; + +@RestController +@EnableAutoConfiguration +@RequestMapping("/device") +public class DeviceController { + + @Autowired private DeviceRepository deviceRepository; + @Autowired private RoomRepository roomRepository; + + @GetMapping + public List getAll(final Principal user) { + return deviceRepository.findAllByUsername(user.getName()); + } + + @PutMapping + public Device update(@Valid @RequestBody DeviceSaveRequest deviceSaveRequest) + throws NotFoundException, BadDataException { + final Device d = + deviceRepository + .findById(deviceSaveRequest.getId()) + .orElseThrow(NotFoundException::new); + + // check if roomId is valid + roomRepository + .findById(deviceSaveRequest.getRoomId()) + .orElseThrow(() -> new BadDataException("roomId is not a valid room id")); + + d.setRoomId(deviceSaveRequest.getRoomId()); + d.setName(deviceSaveRequest.getName()); + + deviceRepository.save(d); + return d; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightController.java index 3220b76..dd2e1e3 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightController.java @@ -29,20 +29,24 @@ public class DimmableLightController { return dimmableLightService.findById(id).orElseThrow(NotFoundException::new); } + private DimmableLight save(DimmableLight initial, DimmableLightSaveRequest dl) { + initial.setIntensity(dl.getIntensity()); + initial.setName(dl.getName()); + initial.setRoomId(dl.getRoomId()); + + return dimmableLightService.save(initial); + } + @PostMapping public DimmableLight create(@Valid @RequestBody DimmableLightSaveRequest dl) { - DimmableLight newDL = new DimmableLight(); - newDL.setIntensity(dl.getIntensity()); - newDL.setId(dl.getId()); - newDL.setName(dl.getName()); - newDL.setRoomId(dl.getRoomId()); - - return dimmableLightService.save(newDL); + return save(new DimmableLight(), dl); } @PutMapping - public DimmableLight update(@Valid @RequestBody DimmableLightSaveRequest dl) { - return this.create(dl); + public DimmableLight update(@Valid @RequestBody DimmableLightSaveRequest sp) + throws NotFoundException { + return save( + dimmableLightService.findById(sp.getId()).orElseThrow(NotFoundException::new), sp); } @DeleteMapping("/{id}") diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/InputDeviceConnectionController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/InputDeviceConnectionController.java new file mode 100644 index 0000000..52f3483 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/InputDeviceConnectionController.java @@ -0,0 +1,92 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import java.util.Set; + +/** + * An abstract controller for an input device that has output connected to it. Aids to create the + * output add and output remove route + * + * @param the type of device this controller is for + * @param the output device attached to I + */ +public abstract class InputDeviceConnectionController< + I extends InputDevice, O extends OutputDevice> { + + private class IOPair { + private final I input; + private final O output; + + private IOPair(I input, O output) { + this.input = input; + this.output = output; + } + } + + private DeviceRepository inputRepository; + + private DeviceRepository outputReposiory; + + private Connector connector; + + /** + * Contstructs the controller by requiring essential object for the controller implementation + * + * @param inputRepository the input device repository + * @param outputRepository the output device repository + * @param connector a appropriate Connector instance for the I and O tyoes. + */ + protected InputDeviceConnectionController( + DeviceRepository inputRepository, + DeviceRepository outputRepository, + Connector connector) { + this.inputRepository = inputRepository; + this.outputReposiory = outputRepository; + this.connector = connector; + } + + private IOPair checkConnectionIDs(Long inputId, Long outputId) throws NotFoundException { + final I input = + inputRepository + .findById(inputId) + .orElseThrow(() -> new NotFoundException("input device")); + final O output = + outputReposiory + .findById(outputId) + .orElseThrow(() -> new NotFoundException("output device")); + return new IOPair(input, output); + } + + /** + * Implements the output device connection creation (add) route + * + * @param inputId input device id + * @param outputId output device id + * @return the list of output devices attached to the input device of id inputId + * @throws NotFoundException if inputId or outputId are not valid + */ + protected Set addOutput(Long inputId, Long outputId) + throws NotFoundException { + final IOPair pair = checkConnectionIDs(inputId, outputId); + connector.connect(pair.input, pair.output, true); + outputReposiory.save(pair.output); + return pair.input.getOutputs(); + } + + /** + * Implements the output device connection destruction (remove) route + * + * @param inputId input device id + * @param outputId output device id + * @return the list of output devices attached to the input device of id inputId + * @throws NotFoundException if inputId or outputId are not valid + */ + protected Set removeOutput(Long inputId, Long outputId) + throws NotFoundException { + final IOPair pair = checkConnectionIDs(inputId, outputId); + connector.connect(pair.input, pair.output, false); + outputReposiory.save(pair.output); + return pair.input.getOutputs(); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/KnobDimmerController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/KnobDimmerController.java index 81f54ea..9f59889 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/KnobDimmerController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/KnobDimmerController.java @@ -2,11 +2,12 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; -import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.KnobDimmerSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveReguest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.KnobDimmerDimRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.KnobDimmer; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.KnobDimmerRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import java.util.List; +import java.util.Set; import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -15,38 +16,70 @@ import org.springframework.web.bind.annotation.*; @RestController @EnableAutoConfiguration @RequestMapping("/knobDimmer") -public class KnobDimmerController { +public class KnobDimmerController + extends InputDeviceConnectionController { - @Autowired private KnobDimmerRepository knobDimmerService; + @Autowired private KnobDimmerRepository knobDimmerRepository; + @Autowired private DimmableLightRepository dimmableLightRepository; + + @Autowired + protected KnobDimmerController( + KnobDimmerRepository inputRepository, DimmableLightRepository outputRepository) { + super( + inputRepository, + outputRepository, + DimmableLight.KNOB_DIMMER_DIMMABLE_LIGHT_CONNECTOR); + this.knobDimmerRepository = inputRepository; + this.dimmableLightRepository = outputRepository; + } @GetMapping public List findAll() { - return toList(knobDimmerService.findAll()); + return toList(knobDimmerRepository.findAll()); } @GetMapping("/{id}") public KnobDimmer findById(@PathVariable("id") long id) throws NotFoundException { - return knobDimmerService.findById(id).orElseThrow(NotFoundException::new); + return knobDimmerRepository.findById(id).orElseThrow(NotFoundException::new); } @PostMapping - public KnobDimmer create(@Valid @RequestBody KnobDimmerSaveRequest kd) { + public KnobDimmer create(@Valid @RequestBody GenericDeviceSaveReguest kd) { KnobDimmer newKD = new KnobDimmer(); - newKD.setLights(kd.getLights()); - newKD.setId(kd.getId()); newKD.setName(kd.getName()); newKD.setRoomId(kd.getRoomId()); - return knobDimmerService.save(newKD); + return knobDimmerRepository.save(newKD); } - @PutMapping - public KnobDimmer update(@Valid @RequestBody KnobDimmerSaveRequest kd) { - return this.create(kd); + @PutMapping("/dimTo") + public Set dimTo(@Valid @RequestBody final KnobDimmerDimRequest bd) + throws NotFoundException { + final KnobDimmer dimmer = + knobDimmerRepository.findById(bd.getId()).orElseThrow(NotFoundException::new); + + dimmer.setLightIntensity(bd.getIntensity()); + dimmableLightRepository.saveAll(dimmer.getOutputs()); + + return dimmer.getOutputs(); + } + + @PostMapping("/{id}/lights") + public Set addLight( + @PathVariable("id") long inputId, @RequestParam("lightId") Long lightId) + throws NotFoundException { + return addOutput(inputId, lightId); + } + + @DeleteMapping("/{id}/lights") + public Set removeLight( + @PathVariable("id") long inputId, @RequestParam("lightId") Long lightId) + throws NotFoundException { + return removeOutput(inputId, lightId); } @DeleteMapping("/{id}") public void delete(@PathVariable("id") long id) { - knobDimmerService.deleteById(id); + knobDimmerRepository.deleteById(id); } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/MotionSensorController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/MotionSensorController.java index a09f900..59c0343 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/MotionSensorController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/MotionSensorController.java @@ -2,10 +2,12 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; -import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.MotionSensorSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveReguest; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.MotionSensor; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.MotionSensorRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.socket.SensorSocketEndpoint; +import java.security.Principal; import java.util.List; import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; @@ -19,6 +21,8 @@ public class MotionSensorController { @Autowired private MotionSensorRepository motionSensorService; + @Autowired private SensorSocketEndpoint sensorSocketEndpoint; + @GetMapping public List findAll() { return toList(motionSensorService.findAll()); @@ -30,19 +34,42 @@ public class MotionSensorController { } @PostMapping - public MotionSensor create(@Valid @RequestBody MotionSensorSaveRequest ms) { + public MotionSensor create(@Valid @RequestBody GenericDeviceSaveReguest ms) { MotionSensor newMS = new MotionSensor(); - newMS.setDetected(ms.isDetected()); - newMS.setId(ms.getId()); newMS.setName(ms.getName()); newMS.setRoomId(ms.getRoomId()); return motionSensorService.save(newMS); } - @PutMapping - public MotionSensor update(@Valid @RequestBody MotionSensorSaveRequest ms) { - return this.create(ms); + /** + * Updates detection status of given motion sensor and propagates update throgh socket + * + * @param sensor the motion sensor to update + * @param detected the new detection status + * @return the updated motion sensor + */ + public MotionSensor updateDetectionFromMotionSensor(MotionSensor sensor, boolean detected) { + sensor.setDetected(detected); + final MotionSensor toReturn = motionSensorService.save(sensor); + + sensorSocketEndpoint.broadcast(sensor, motionSensorService.findUser(sensor.getId())); + + return toReturn; + } + + @PutMapping("/{id}/detect") + public MotionSensor updateDetection( + @PathVariable("id") Long sensorId, + @RequestParam("detected") boolean detected, + final Principal principal) + throws NotFoundException { + + return updateDetectionFromMotionSensor( + motionSensorService + .findByIdAndUsername(sensorId, principal.getName()) + .orElseThrow(NotFoundException::new), + detected); } @DeleteMapping("/{id}") diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightController.java index 061f12c..15ce2e8 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightController.java @@ -36,10 +36,7 @@ public class RegularLightController { return regularLightService.findById(id).orElseThrow(NotFoundException::new); } - @PostMapping - public RegularLight create(@Valid @RequestBody RegularLightSaveRequest rl) { - RegularLight newRL = new RegularLight(); - newRL.setId(rl.getId()); + private RegularLight save(RegularLight newRL, RegularLightSaveRequest rl) { newRL.setName(rl.getName()); newRL.setRoomId(rl.getRoomId()); newRL.setOn(rl.isOn()); @@ -47,9 +44,16 @@ public class RegularLightController { return regularLightService.save(newRL); } + @PostMapping + public RegularLight create(@Valid @RequestBody RegularLightSaveRequest rl) { + return save(new RegularLight(), rl); + } + @PutMapping - public RegularLight update(@Valid @RequestBody RegularLightSaveRequest rl) { - return this.create(rl); + public RegularLight update(@Valid @RequestBody RegularLightSaveRequest rl) + throws NotFoundException { + return save( + regularLightService.findById(rl.getId()).orElseThrow(NotFoundException::new), rl); } @DeleteMapping("/{id}") diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomController.java index 3a2ba78..fb42037 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomController.java @@ -39,17 +39,17 @@ public class RoomController { final String username = principal.getName(); final Long userId = userRepository.findByUsername(username).getId(); final String img = r.getImage(); - final String icon = r.getIcon(); + final Room.Icon icon = r.getIcon(); newRoom.setUserId(userId); newRoom.setName(r.getName()); if (img != null) { - newRoom.setImage(img.getBytes()); + newRoom.setImage(img); } else if (setWhenNull) { newRoom.setImage(null); } if (icon != null) { - newRoom.setIcon(icon.getBytes()); + newRoom.setIcon(icon); } else if (setWhenNull) { newRoom.setIcon(null); } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SensorController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SensorController.java index d738a37..ee1be81 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SensorController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SensorController.java @@ -5,6 +5,9 @@ import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SensorSaveRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import ch.usi.inf.sa4.sanmarinoes.smarthut.socket.SensorSocketEndpoint; +import java.math.BigDecimal; +import java.security.Principal; import java.util.*; import java.util.List; import javax.validation.Valid; @@ -19,6 +22,8 @@ public class SensorController { @Autowired private SensorRepository sensorRepository; + @Autowired private SensorSocketEndpoint sensorSocketEndpoint; + @GetMapping public List findAll() { return toList(sensorRepository.findAll()); @@ -33,17 +38,40 @@ public class SensorController { public Sensor create(@Valid @RequestBody SensorSaveRequest s) { Sensor newSensor = new Sensor(); newSensor.setSensor(s.getSensor()); - newSensor.setValue(s.getValue()); - newSensor.setId(s.getId()); newSensor.setName(s.getName()); newSensor.setRoomId(s.getRoomId()); + newSensor.setValue(s.getValue()); return sensorRepository.save(newSensor); } - @PutMapping - public Sensor update(@Valid @RequestBody SensorSaveRequest s) { - return this.create(s); + /** + * Updates the sensor with new measurement and propagates update through websocket + * + * @param sensor the sensor to update + * @param value the new measurement + * @return the updated sensor + */ + public Sensor updateValueFromSensor(Sensor sensor, BigDecimal value) { + sensor.setValue(value); + final Sensor toReturn = sensorRepository.save(sensor); + + sensorSocketEndpoint.broadcast(sensor, sensorRepository.findUser(sensor.getId())); + + return toReturn; + } + + @PutMapping("/{id}/value") + public Sensor updateValue( + @PathVariable("id") Long sensorId, + @RequestParam("value") BigDecimal value, + final Principal principal) + throws NotFoundException { + return updateValueFromSensor( + sensorRepository + .findByIdAndUsername(sensorId, principal.getName()) + .orElseThrow(NotFoundException::new), + value); } @DeleteMapping("/{id}") diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugController.java index 43b66d9..8a57430 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugController.java @@ -29,9 +29,7 @@ public class SmartPlugController { return smartPlugRepository.findById(id).orElseThrow(NotFoundException::new); } - @PostMapping - public SmartPlug create(@Valid @RequestBody SmartPlugSaveRequest sp) { - SmartPlug newSP = new SmartPlug(); + private SmartPlug save(SmartPlug newSP, SmartPlugSaveRequest sp) { newSP.setOn(sp.isOn()); newSP.setId(sp.getId()); newSP.setName(sp.getName()); @@ -40,9 +38,15 @@ public class SmartPlugController { return smartPlugRepository.save(newSP); } + @PostMapping + public SmartPlug create(@Valid @RequestBody SmartPlugSaveRequest sp) { + return save(new SmartPlug(), sp); + } + @PutMapping - public SmartPlug update(@Valid @RequestBody SmartPlugSaveRequest sp) { - return this.create(sp); + public SmartPlug update(@Valid @RequestBody SmartPlugSaveRequest sp) throws NotFoundException { + return save( + smartPlugRepository.findById(sp.getId()).orElseThrow(NotFoundException::new), sp); } @DeleteMapping("/{id}") diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchController.java index 5df72cf..f90108c 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchController.java @@ -2,7 +2,8 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; -import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SwitchSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveReguest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SwitchOperationRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import java.util.*; @@ -15,9 +16,24 @@ import org.springframework.web.bind.annotation.*; @RestController @EnableAutoConfiguration @RequestMapping("/switch") -public class SwitchController { +public class SwitchController extends InputDeviceConnectionController { - @Autowired private SwitchRepository switchRepository; + private SwitchRepository switchRepository; + private SwitchableRepository switchableRepository; + + /** + * Contstructs the controller by requiring essential object for the controller implementation + * + * @param inputRepository the input device repository + * @param outputRepository the output device repository + */ + @Autowired + protected SwitchController( + SwitchRepository inputRepository, SwitchableRepository outputRepository) { + super(inputRepository, outputRepository, Switchable.SWITCH_SWITCHABLE_CONNECTOR); + this.switchRepository = inputRepository; + this.switchableRepository = outputRepository; + } @GetMapping public List findAll() { @@ -30,19 +46,48 @@ public class SwitchController { } @PostMapping - public Switch create(@Valid @RequestBody SwitchSaveRequest s) { + public Switch create(@Valid @RequestBody GenericDeviceSaveReguest s) { Switch newSwitch = new Switch(); - newSwitch.setId(s.getId()); newSwitch.setName(s.getName()); newSwitch.setRoomId(s.getRoomId()); - newSwitch.setOn(s.isOn()); return switchRepository.save(newSwitch); } - @PutMapping - public Switch update(@Valid @RequestBody SwitchSaveRequest s) { - return this.create(s); + @PutMapping("/operate") + public Set operate(@Valid @RequestBody final SwitchOperationRequest sr) + throws NotFoundException { + final Switch s = switchRepository.findById(sr.getId()).orElseThrow(NotFoundException::new); + + switch (sr.getType()) { + case ON: + s.setOn(true); + break; + case OFF: + s.setOn(false); + break; + case TOGGLE: + s.toggle(); + break; + } + + switchableRepository.saveAll(s.getOutputs()); + + return s.getOutputs(); + } + + @PostMapping("/{id}/lights") + public Set addSwitchable( + @PathVariable("id") long inputId, @RequestParam("switchableId") Long switchableId) + throws NotFoundException { + return addOutput(inputId, switchableId); + } + + @DeleteMapping("/{id}/lights") + public Set removeSwitchable( + @PathVariable("id") long inputId, @RequestParam("switchableId") Long switchableId) + throws NotFoundException { + return removeOutput(inputId, switchableId); } @DeleteMapping("/{id}") diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/WelcomeController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/WelcomeController.java deleted file mode 100644 index a81eec6..0000000 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/WelcomeController.java +++ /dev/null @@ -1,15 +0,0 @@ -package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; - -import org.springframework.boot.autoconfigure.*; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@RestController -@EnableAutoConfiguration -public class WelcomeController { - - @GetMapping - ResponseEntity testConnection() { - return ResponseEntity.ok(null); - } -} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ButtonDimmerDimRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ButtonDimmerDimRequest.java new file mode 100644 index 0000000..8e07015 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ButtonDimmerDimRequest.java @@ -0,0 +1,34 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; + +import javax.validation.constraints.NotNull; + +/** A 'dim' event from a button dimmer. */ +public class ButtonDimmerDimRequest { + + /** The device id */ + @NotNull private Long id; + + public enum DimType { + UP, + DOWN; + } + + /** Whether the dim is up or down */ + @NotNull private DimType dimType; + + public DimType getDimType() { + return dimType; + } + + public void setDimType(DimType dimType) { + this.dimType = dimType; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ButtonDimmerSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ButtonDimmerSaveRequest.java deleted file mode 100644 index 31a22d8..0000000 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ButtonDimmerSaveRequest.java +++ /dev/null @@ -1,63 +0,0 @@ -package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; - -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLight; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Room; -import java.util.HashSet; -import java.util.Set; -import javax.persistence.*; -import javax.validation.constraints.NotNull; - -public class ButtonDimmerSaveRequest { - @Lob private Set lights = new HashSet(); - - /** Device identifier */ - private long id; - - /** The room this device belongs in */ - private Room room; - - /** - * The room this device belongs in, as a foreign key id. To use when updating and inserting from - * a REST call. - */ - @NotNull private Long roomId; - - /** The name of the device as assigned by the user (e.g. 'Master bedroom light') */ - @NotNull private String name; - - public Set getLights() { - return this.lights; - } - - public void setLights(Set newLights) { - this.lights = newLights; - } - - public void setRoom(Room room) { - this.room = room; - } - - public void setRoomId(Long roomId) { - this.roomId = roomId; - } - - public void setName(String name) { - this.name = name; - } - - public long getId() { - return id; - } - - public Room getRoom() { - return room; - } - - public Long getRoomId() { - return roomId; - } - - public String getName() { - return name; - } -} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DeviceSaveRequest.java similarity index 75% rename from src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchSaveRequest.java rename to src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DeviceSaveRequest.java index c7516f2..a975117 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchSaveRequest.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DeviceSaveRequest.java @@ -1,11 +1,9 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; +import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; -public class SwitchSaveRequest { - /** The state of this switch */ - private boolean on; - +public class DeviceSaveRequest { /** Device identifier */ private long id; @@ -16,33 +14,29 @@ public class SwitchSaveRequest { @NotNull private Long roomId; /** The name of the device as assigned by the user (e.g. 'Master bedroom light') */ - @NotNull private String name; - - public void setRoomId(Long roomId) { - this.roomId = roomId; - } - - public void setName(String name) { - this.name = name; - } + @NotNull @NotEmpty private String name; public long getId() { return id; } + public void setId(long id) { + this.id = id; + } + public Long getRoomId() { return roomId; } + public void setRoomId(Long roomId) { + this.roomId = roomId; + } + public String getName() { return name; } - public boolean isOn() { - return on; - } - - public void setOn(boolean on) { - this.on = on; + public void setName(String name) { + this.name = name; } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableLightSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableLightSaveRequest.java index 8edff94..01bec1a 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableLightSaveRequest.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableLightSaveRequest.java @@ -1,24 +1,20 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Room; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; public class DimmableLightSaveRequest { + /** Device id (used only for update requests) */ + private Long id; + /** The light intensity value. Goes from 0 (off) to 100 (on) */ @NotNull @Min(1) @Max(100) private Integer intensity = 0; - /** Device identifier */ - private long id; - - /** The room this device belongs in */ - private Room room; - /** * The room this device belongs in, as a foreign key id. To use when updating and inserting from * a REST call. @@ -28,10 +24,6 @@ public class DimmableLightSaveRequest { /** The name of the device as assigned by the user (e.g. 'Master bedroom light') */ @NotNull private String name; - public void setRoom(Room room) { - this.room = room; - } - public void setRoomId(Long roomId) { this.roomId = roomId; } @@ -40,14 +32,6 @@ public class DimmableLightSaveRequest { this.name = name; } - public long getId() { - return id; - } - - public Room getRoom() { - return room; - } - public Long getRoomId() { return roomId; } @@ -60,10 +44,15 @@ public class DimmableLightSaveRequest { return intensity; } - public void setIntensity(Integer intensity) throws IllegalArgumentException { - if (intensity < 0 || intensity > 100) { - throw new IllegalArgumentException("The intensity level can't go below 0 or above 100"); - } + public void setIntensity(Integer intensity) { this.intensity = intensity; } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/MotionSensorSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GenericDeviceSaveReguest.java similarity index 66% rename from src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/MotionSensorSaveRequest.java rename to src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GenericDeviceSaveReguest.java index ba73495..8ec2671 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/MotionSensorSaveRequest.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GenericDeviceSaveReguest.java @@ -2,12 +2,7 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; import javax.validation.constraints.NotNull; -public class MotionSensorSaveRequest { - private boolean detected; - - /** Device identifier */ - private long id; - +public class GenericDeviceSaveReguest { /** * The room this device belongs in, as a foreign key id. To use when updating and inserting from * a REST call. @@ -25,10 +20,6 @@ public class MotionSensorSaveRequest { this.name = name; } - public long getId() { - return id; - } - public Long getRoomId() { return roomId; } @@ -36,12 +27,4 @@ public class MotionSensorSaveRequest { public String getName() { return name; } - - public boolean isDetected() { - return detected; - } - - public void setDetected(boolean detected) { - this.detected = detected; - } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/KnobDimmerDimRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/KnobDimmerDimRequest.java new file mode 100644 index 0000000..6df303a --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/KnobDimmerDimRequest.java @@ -0,0 +1,33 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +public class KnobDimmerDimRequest { + + /** The device id */ + @NotNull private Long id; + + /** The absolute intensity value */ + @NotNull + @Min(0) + @Max(100) + private Integer intensity; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Integer getIntensity() { + return intensity; + } + + public void setIntensity(Integer intensity) { + this.intensity = intensity; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/KnobDimmerSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/KnobDimmerSaveRequest.java deleted file mode 100644 index ce053e3..0000000 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/KnobDimmerSaveRequest.java +++ /dev/null @@ -1,51 +0,0 @@ -package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; - -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLight; -import java.util.HashSet; -import java.util.Set; -import javax.persistence.Lob; -import javax.validation.constraints.NotNull; - -public class KnobDimmerSaveRequest { - @Lob private Set lights = new HashSet(); - - /** Device identifier */ - private long id; - - /** - * The room this device belongs in, as a foreign key id. To use when updating and inserting from - * a REST call. - */ - @NotNull private Long roomId; - - /** The name of the device as assigned by the user (e.g. 'Master bedroom light') */ - @NotNull private String name; - - public void setRoomId(Long roomId) { - this.roomId = roomId; - } - - public void setName(String name) { - this.name = name; - } - - public long getId() { - return id; - } - - public Long getRoomId() { - return roomId; - } - - public String getName() { - return name; - } - - public void setLights(Set lights) { - this.lights = lights; - } - - public Set getLights() { - return lights; - } -} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RegularLightSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RegularLightSaveRequest.java index ac1324d..99211e5 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RegularLightSaveRequest.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RegularLightSaveRequest.java @@ -45,4 +45,8 @@ public class RegularLightSaveRequest { public void setOn(boolean on) { this.on = on; } + + public void setId(long id) { + this.id = id; + } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RoomSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RoomSaveRequest.java index f813b1c..ca8fa0f 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RoomSaveRequest.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RoomSaveRequest.java @@ -1,17 +1,19 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Room; import javax.persistence.Lob; import javax.validation.constraints.NotNull; public class RoomSaveRequest { + + @NotNull private Room.Icon icon; + /** - * Icon and image are to be given as byte[]. In order to get an encoded string from it, the + * Image is to be given as byte[]. In order to get an encoded string from it, the * Base64.getEncoder().encodeToString(byte[] content) should be used. For further information: * https://www.baeldung.com/java-base64-image-string * https://docs.oracle.com/javase/8/docs/api/java/util/Base64.html */ - @Lob private String icon; - @Lob private String image; /** The user given name of this room (e.g. 'Master bedroom') */ @@ -25,11 +27,11 @@ public class RoomSaveRequest { this.name = name; } - public String getIcon() { + public Room.Icon getIcon() { return icon; } - public void setIcon(String icon) { + public void setIcon(Room.Icon icon) { this.icon = icon; } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SensorSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SensorSaveRequest.java index 421523c..62b0b5e 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SensorSaveRequest.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SensorSaveRequest.java @@ -2,6 +2,7 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Sensor; import com.google.gson.annotations.SerializedName; +import java.math.BigDecimal; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.validation.constraints.NotNull; @@ -23,16 +24,12 @@ public class SensorSaveRequest { LIGHT } - /** The value of this sensor according to its sensor type */ - private int value; - /** The type of this sensor */ @NotNull @Enumerated(value = EnumType.STRING) private Sensor.SensorType sensor; - /** Device identifier */ - private long id; + @NotNull private BigDecimal value; /** * The room this device belongs in, as a foreign key id. To use when updating and inserting from @@ -51,10 +48,6 @@ public class SensorSaveRequest { this.name = name; } - public long getId() { - return id; - } - public Long getRoomId() { return roomId; } @@ -71,11 +64,11 @@ public class SensorSaveRequest { this.sensor = sensor; } - public int getValue() { - return this.value; + public BigDecimal getValue() { + return value; } - public void setValue(int newValue) { - this.value = newValue; + public void setValue(BigDecimal value) { + this.value = value; } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SmartPlugSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SmartPlugSaveRequest.java index 3318505..6b2f9b5 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SmartPlugSaveRequest.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SmartPlugSaveRequest.java @@ -45,4 +45,8 @@ public class SmartPlugSaveRequest { public void setOn(boolean on) { this.on = on; } + + public void setId(long id) { + this.id = id; + } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchOperationRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchOperationRequest.java new file mode 100644 index 0000000..3fb552b --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchOperationRequest.java @@ -0,0 +1,35 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; + +import javax.validation.constraints.NotNull; + +/** An on/off/toggle operation on a switch */ +public class SwitchOperationRequest { + + /** The device id */ + @NotNull private Long id; + + public enum OperationType { + ON, + OFF, + TOGGLE + } + + /** The type of switch operation */ + @NotNull private SwitchOperationRequest.OperationType type; + + public OperationType getType() { + return type; + } + + public void setType(OperationType type) { + this.type = type; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/BadDataException.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/BadDataException.java new file mode 100644 index 0000000..2c6c4d4 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/BadDataException.java @@ -0,0 +1,11 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.error; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.BAD_REQUEST) +public class BadDataException extends Exception { + public BadDataException(String message) { + super(message); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/NotFoundException.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/NotFoundException.java index 1d5f90d..471107f 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/NotFoundException.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/NotFoundException.java @@ -6,6 +6,10 @@ import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(code = HttpStatus.NOT_FOUND) public class NotFoundException extends Exception { public NotFoundException() { - super("Not Found"); + super("Not found"); + } + + public NotFoundException(String what) { + super(what + " not found"); } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ButtonDimmer.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ButtonDimmer.java index 1e5f702..fc91c22 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ButtonDimmer.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ButtonDimmer.java @@ -1,9 +1,6 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; - -import java.util.Set; import javax.persistence.Entity; -import javax.persistence.OneToMany; /** * Represents a dimmer that can only instruct an increase or decrease of intensity (i.e. like a @@ -11,60 +8,25 @@ import javax.persistence.OneToMany; */ @Entity public class ButtonDimmer extends Dimmer { + + /** The delta amount to apply to a increase or decrease intensity */ + private static final int DIM_INCREMENT = 10; + public ButtonDimmer() { super("button-dimmer"); } - @OneToMany(mappedBy = "dimmer") - private Set lights; - - /** Increases the current intensity level of the dimmable light by 1 */ + /** Increases the current intensity level of the dimmable light by DIM_INCREMENT */ public void increaseIntensity() { - for (DimmableLight dl : lights) { - dl.setIntensity(dl.getIntensity() + 1); + for (DimmableLight dl : getOutputs()) { + dl.setIntensity(dl.getIntensity() + DIM_INCREMENT); } } - /** Decreases the current intensity level of the dimmable light by 1 */ + /** Decreases the current intensity level of the dimmable light by DIM_INCREMENT */ public void decreaseIntensity() { - for (DimmableLight dl : lights) { - dl.setIntensity(dl.getIntensity() - 1); + for (DimmableLight dl : getOutputs()) { + dl.setIntensity(dl.getIntensity() - DIM_INCREMENT); } } - - /** - * Adds a DimmableLight to this set of DimmableLights - * - * @param dl The DimmableLight to be added - */ - public void addLight(DimmableLight dl) { - lights.add(dl); - } - - /** - * Removes the given DimmableLight - * - * @param dl The DimmableLight to be removed - */ - public void removeLight(DimmableLight dl) { - lights.remove(dl); - } - - /** Clears this set */ - public void clearSet() { - lights.clear(); - } - - /** - * Get the lights - * - * @return duh - */ - public Set getLights() { - return this.lights; - } - - public void setLights(Set newLights) { - this.lights = newLights; - } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java index 9bf3791..40c6a17 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java @@ -6,6 +6,8 @@ import org.springframework.data.repository.CrudRepository; public interface ConfirmationTokenRepository extends CrudRepository { ConfirmationToken findByConfirmationToken(String confirmationToken); + ConfirmationToken findByUser(User user); + @Transactional void deleteByUserAndResetPassword(User user, boolean resetPassword); } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Connector.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Connector.java new file mode 100644 index 0000000..91b1e88 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Connector.java @@ -0,0 +1,47 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * A rule on how to connect an input device type to an output device type + * + * @param the input device type + * @param the output device type + */ +@FunctionalInterface +public interface Connector { + + /** + * Connects or disconnects input to output + * + * @param input the input device + * @param output the output device + * @param connect true if connection, false if disconnection + */ + void connect(I input, O output, boolean connect); + + /** + * Produces a basic implementation of a connector, assuming there is a OneToMany relationship + * between J and K + * + * @param outputsGetter the getter method of the set of outputs on the input class + * @param inputSetter the setter method for the input id on the output class + * @param the input device type + * @param the output device type + * @return a Connector implementation for the pair of types J and K + */ + static Connector basic( + Function> outputsGetter, BiConsumer inputSetter) { + return (i, o, connect) -> { + if (connect) { + outputsGetter.apply(i).add(o); + } else { + outputsGetter.apply(i).remove(o); + } + + inputSetter.accept(o, connect ? i.getId() : null); + }; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java index 7aa65e8..6d0333d 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java @@ -1,5 +1,6 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude; import com.google.gson.annotations.SerializedName; import io.swagger.annotations.ApiModelProperty; import javax.persistence.*; @@ -26,11 +27,16 @@ public abstract class Device { @ApiModelProperty(hidden = true) private long id; + @ManyToOne + @JoinColumn(name = "room_id", updatable = false, insertable = false) + @GsonExclude + private Room room; + /** * The room this device belongs in, as a foreign key id. To use when updating and inserting from * a REST call. */ - @Column(name = "room_id", nullable = false) + @Column(name = "room_id", nullable = false, unique = true) @NotNull private Long roomId; @@ -43,17 +49,13 @@ public abstract class Device { * The name for the category of this particular device (e.g 'dimmer'). Not stored in the * database but set thanks to constructors */ - @ApiModelProperty(hidden = true) - @Transient - private final String kind; + @Transient private final String kind; /** * The way this device behaves in the automation flow. Not stored in the database but set thanks * to constructors */ - @ApiModelProperty(hidden = true) - @Transient - private final FlowType flowType; + @Transient private final FlowType flowType; public long getId() { return id; diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DeviceRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DeviceRepository.java index fdae66e..f844882 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DeviceRepository.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DeviceRepository.java @@ -1,6 +1,8 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; @@ -10,4 +12,32 @@ import org.springframework.data.repository.query.Param; */ public interface DeviceRepository extends CrudRepository { List findByRoomId(@Param("roomId") long roomId); + + /** + * Finds devices by their id and a username + * + * @param id the device id + * @param username a User's username + * @return an optional device, empty if none found + */ + @Query("SELECT d FROM Device d JOIN d.room r JOIN r.user u WHERE d.id = ?1 AND u.username = ?2") + Optional findByIdAndUsername(Long id, String username); + + /** + * Finds all devices belonging to a user + * + * @param username the User's username + * @return all devices of that user + */ + @Query("SELECT d FROM Device d JOIN d.room r JOIN r.user u WHERE u.username = ?1") + List findAllByUsername(String username); + + /** + * Find the user associated with a device through a room + * + * @param deviceId the device id + * @return a user object + */ + @Query("SELECT u FROM Device d JOIN d.room r JOIN r.user u WHERE d.id = ?1") + User findUser(Long deviceId); } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLight.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLight.java index a71afbc..9a75471 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLight.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLight.java @@ -1,7 +1,9 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.validation.constraints.Max; import javax.validation.constraints.Min; @@ -9,18 +11,31 @@ import javax.validation.constraints.NotNull; /** Represent a dimmable light */ @Entity -public class DimmableLight extends Light { +public class DimmableLight extends Switchable { + + public static final Connector + BUTTON_DIMMER_DIMMABLE_LIGHT_CONNECTOR = + Connector.basic(ButtonDimmer::getOutputs, DimmableLight::setDimmerId); + + public static final Connector KNOB_DIMMER_DIMMABLE_LIGHT_CONNECTOR = + Connector.basic(KnobDimmer::getOutputs, DimmableLight::setDimmerId); public DimmableLight() { super("light"); } - @ManyToOne private Dimmer dimmer; + @ManyToOne + @GsonExclude + @JoinColumn(name = "dimmer_id", updatable = false, insertable = false) + private Dimmer dimmer; + + @Column(name = "dimmer_id") + private Long dimmerId; /** The light intensity value. Goes from 0 (off) to 100 (on) */ @NotNull @Column(nullable = false) - @Min(1) + @Min(0) @Max(100) private Integer intensity = 0; @@ -28,10 +43,41 @@ public class DimmableLight extends Light { return intensity; } - public void setIntensity(Integer intensity) throws IllegalArgumentException { - if (intensity < 0 || intensity > 100) { - throw new IllegalArgumentException("The intensity level can't go below 0 or above 100"); + /** + * Sets the intensity to a certain level. Out of bound values are corrected to the respective + * extremums. An intensity level of 0 turns the light off, but keeps the old intensity level + * stored. + * + * @param intensity the intensity level (may be out of bounds) + */ + public void setIntensity(Integer intensity) { + if (intensity <= 0) { + this.intensity = 0; + } else if (intensity > 100) { + this.intensity = 100; + } else { + this.intensity = intensity; } - this.intensity = intensity; + } + + @Override + public boolean isOn() { + return intensity != 0; + } + + @Override + public void setOn(boolean on) { + intensity = on ? 100 : 0; + } + + public void setDimmerId(Long dimmerId) { + this.dimmerId = dimmerId; + super.setSwitchId(null); + }; + + @Override + public void setSwitchId(Long switchId) { + super.setSwitchId(switchId); + this.dimmerId = null; } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLightRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLightRepository.java index 484084b..a32b3c6 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLightRepository.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLightRepository.java @@ -1,3 +1,3 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; -public interface DimmableLightRepository extends DeviceRepository {} +public interface DimmableLightRepository extends SwitchableRepository {} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmer.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmer.java index 2658b35..af00025 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmer.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmer.java @@ -1,8 +1,10 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; +import java.util.Set; import javax.persistence.Entity; import javax.persistence.Inheritance; import javax.persistence.InheritanceType; +import javax.persistence.OneToMany; /** Represents a generic dimmer input device */ @Entity @@ -11,4 +13,17 @@ public abstract class Dimmer extends InputDevice { public Dimmer(String kind) { super(kind); } + + @OneToMany(mappedBy = "dimmer") + private Set lights; + + /** + * Get the lights connected to this dimmer + * + * @return duh + */ + @Override + public Set getOutputs() { + return this.lights; + } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/InputDevice.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/InputDevice.java index e632178..da45b67 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/InputDevice.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/InputDevice.java @@ -1,14 +1,22 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; +import java.util.Set; import javax.persistence.Entity; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; /** * A generic abstraction for an input device, i.e. something that captures input either from the * environment (sensor) or the user (switch / dimmer). */ @Entity +@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) public abstract class InputDevice extends Device { public InputDevice(String kind) { super(kind, FlowType.INPUT); } + + public Set getOutputs() { + return Set.of(); + } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/KnobDimmer.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/KnobDimmer.java index 0a90998..ce3745c 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/KnobDimmer.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/KnobDimmer.java @@ -1,8 +1,6 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; -import java.util.Set; import javax.persistence.Entity; -import javax.persistence.OneToMany; /** * Represents a dimmer able to set absolute intensity values (i.e. knowing the absolute intensity @@ -10,62 +8,19 @@ import javax.persistence.OneToMany; */ @Entity public class KnobDimmer extends Dimmer { + public KnobDimmer() { super("knob-dimmer"); } - @OneToMany(mappedBy = "dimmer") - private Set lights; - /** - * Increases or decreases the current intensity level by 5, moving between absolute multiples of - * 5 between 0 and 100, of all dimmable lights mapped to this knob + * Sets absolutely the intensity level of all lights connected * - * @param inc The direction the knob is turned with + * @param intensity the intensity (must be from 0 to 100) */ - public void modifyIntensity(boolean inc) { - - for (DimmableLight dl : lights) { - int remainder = dl.getIntensity() / 5; - - if (inc) { - dl.setIntensity(dl.getIntensity() - remainder); - dl.setIntensity((dl.getIntensity() + 5) % 105); - } else { - dl.setIntensity(dl.getIntensity() + (5 - remainder)); - dl.setIntensity((dl.getIntensity() - 5) % 105); - } + public void setLightIntensity(int intensity) { + for (DimmableLight dl : getOutputs()) { + dl.setIntensity(intensity); } } - - /** - * Adds a DimmableLight to this set of DimmableLights - * - * @param dl The DimmableLight to be added - */ - public void addLight(DimmableLight dl) { - lights.add(dl); - } - - /** - * Removes the given DimmableLight - * - * @param dl The DimmableLight to be removed - */ - public void removeLight(DimmableLight dl) { - lights.remove(dl); - } - - /** Clears this set */ - public void clearSet() { - lights.clear(); - } - - public void setLights(Set lights) { - this.lights = lights; - } - - public Set getLights() { - return lights; - } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Light.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Light.java deleted file mode 100644 index 68a819b..0000000 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Light.java +++ /dev/null @@ -1,31 +0,0 @@ -package ch.usi.inf.sa4.sanmarinoes.smarthut.models; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Inheritance; -import javax.persistence.InheritanceType; -import javax.validation.constraints.NotNull; - -/** Represents a generic light */ -@Entity -@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) -public abstract class Light extends OutputDevice { - - /** Whether the light is on or not */ - @Column(name = "light_on", nullable = false) - @NotNull - boolean on; - - protected Light(String kind) { - super(kind); - this.on = false; - } - - public boolean isOn() { - return on; - } - - public void setOn(boolean on) { - this.on = on; - } -} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OutputDevice.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OutputDevice.java index 39e5dd0..c5b401f 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OutputDevice.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OutputDevice.java @@ -1,8 +1,6 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; -import javax.persistence.Entity; -import javax.persistence.Inheritance; -import javax.persistence.InheritanceType; +import javax.persistence.*; /** * Represents a generic output device, i.e. something that causes some behaviour (light, smartPlugs, diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLight.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLight.java index 8f07377..de9c10a 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLight.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLight.java @@ -1,11 +1,30 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; +import javax.persistence.Column; import javax.persistence.Entity; +import javax.validation.constraints.NotNull; /** Represents a standard non-dimmable light */ @Entity -public class RegularLight extends Light { +public class RegularLight extends Switchable { + + /** Whether the light is on or not */ + @Column(name = "light_on", nullable = false) + @NotNull + boolean on; + public RegularLight() { super("regular-light"); + this.on = false; + } + + @Override + public boolean isOn() { + return on; + } + + @Override + public void setOn(boolean on) { + this.on = on; } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLightRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLightRepository.java index 43c4e17..cad8831 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLightRepository.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLightRepository.java @@ -1,3 +1,3 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; -public interface RegularLightRepository extends DeviceRepository {} +public interface RegularLightRepository extends SwitchableRepository {} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java index e15805b..a30e4f8 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java @@ -1,5 +1,7 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude; +import com.google.gson.annotations.SerializedName; import io.swagger.annotations.ApiModelProperty; import javax.persistence.*; import javax.validation.constraints.NotNull; @@ -8,25 +10,129 @@ import javax.validation.constraints.NotNull; @Entity public class Room { + /** A collection of Semantic UI icons */ + @SuppressWarnings("unused") + public enum Icon { + @SerializedName("home") + HOME("home"), + @SerializedName("coffee") + COFFEE("coffee"), + @SerializedName("beer") + BEER("beer"), + @SerializedName("glass martini") + GLASS_MARTINI("glass martini"), + @SerializedName("film") + FILM("film"), + @SerializedName("video") + VIDEO("video"), + @SerializedName("music") + MUSIC("music"), + @SerializedName("headphones") + HEADPHONES("headphones"), + @SerializedName("fax") + FAX("fax"), + @SerializedName("phone") + PHONE("phone"), + @SerializedName("laptop") + LAPTOP("laptop"), + @SerializedName("bath") + BATH("bath"), + @SerializedName("shower") + SHOWER("shower"), + @SerializedName("bed") + BED("bed"), + @SerializedName("child") + CHILD("child"), + @SerializedName("warehouse") + WAREHOUSE("warehouse"), + @SerializedName("car") + CAR("car"), + @SerializedName("bicycle") + BICYCLE("bicycle"), + @SerializedName("motorcycle") + MOTORCYCLE("motorcycle"), + @SerializedName("archive") + ARCHIVE("archive"), + @SerializedName("boxes") + BOXES("boxes"), + @SerializedName("cubes") + CUBES("cubes"), + @SerializedName("chess") + CHESS("chess"), + @SerializedName("gamepad") + GAMEPAD("gamepad"), + @SerializedName("futbol") + FUTBOL("futbol"), + @SerializedName("table tennis") + TABLE_TENNIS("table tennis"), + @SerializedName("server") + SERVER("server"), + @SerializedName("tv") + TV("tv"), + @SerializedName("heart") + HEART("heart"), + @SerializedName("camera") + CAMERA("camera"), + @SerializedName("trophy") + TROPHY("trophy"), + @SerializedName("wrench") + WRENCH("wrench"), + @SerializedName("image") + IMAGE("image"), + @SerializedName("book") + BOOK("book"), + @SerializedName("university") + UNIVERSITY("university"), + @SerializedName("medkit") + MEDKIT("medkit"), + @SerializedName("paw") + PAW("paw"), + @SerializedName("tree") + TREE("tree"), + @SerializedName("utensils") + UTENSILS("utensils"), + @SerializedName("male") + MALE("male"), + @SerializedName("female") + FEMALE("female"), + @SerializedName("life ring outline") + LIFE_RING_OUTLINE("life ring outline"); + + private String iconName; + + Icon(String s) { + this.iconName = s; + } + + @Override + public String toString() { + return iconName; + } + } + @Id @GeneratedValue(strategy = GenerationType.AUTO) - @Column(name = "id", updatable = false, nullable = false) + @Column(name = "id", updatable = false, nullable = false, unique = true) @ApiModelProperty(hidden = true) private Long id; + /** The room icon, out of a set of Semantic UI icons */ + @Column private Icon icon; + /** - * Icon and image are to be given as byte[]. In order to get an encoded string from it, the + * Image is to be given as byte[]. In order to get an encoded string from it, the * Base64.getEncoder().encodeToString(byte[] content) should be used. For further information: * https://www.baeldung.com/java-base64-image-string * https://docs.oracle.com/javase/8/docs/api/java/util/Base64.html */ - @Lob - @Column(name = "icon", columnDefinition = "TEXT") - private byte[] icon; - @Lob @Column(name = "image", columnDefinition = "TEXT") - private byte[] image; + private String image; + + @ManyToOne + @JoinColumn(name = "user_id", updatable = false, insertable = false) + @GsonExclude + private User user; /** * User that owns the house this room is in as a foreign key id. To use when updating and @@ -65,19 +171,19 @@ public class Room { this.name = name; } - public byte[] getIcon() { + public Icon getIcon() { return icon; } - public void setIcon(byte[] icon) { + public void setIcon(Icon icon) { this.icon = icon; } - public byte[] getImage() { + public String getImage() { return image; } - public void setImage(byte[] image) { + public void setImage(String image) { this.image = image; } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Sensor.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Sensor.java index e352a52..525ceb3 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Sensor.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Sensor.java @@ -1,6 +1,8 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; import com.google.gson.annotations.SerializedName; +import java.math.BigDecimal; +import java.util.Map; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EnumType; @@ -11,6 +13,12 @@ import javax.validation.constraints.NotNull; @Entity public class Sensor extends InputDevice { + public static final Map TYPICAL_VALUES = + Map.of( + SensorType.TEMPERATURE, new BigDecimal(17.0), + SensorType.HUMIDITY, new BigDecimal(40.0), + SensorType.LIGHT, new BigDecimal(1000)); + /** Type of sensor, i.e. of the thing the sensor measures. */ public enum SensorType { /** A sensor that measures temperature in degrees celsius */ @@ -27,8 +35,8 @@ public class Sensor extends InputDevice { } /** The value of this sensor according to its sensor type */ - @Column(nullable = false) - private int value; + @Column(nullable = false, length = 10, precision = 1) + private BigDecimal value; /** The type of this sensor */ @Column(nullable = false) @@ -44,15 +52,20 @@ public class Sensor extends InputDevice { this.sensor = sensor; } - public int getValue() { + public BigDecimal getValue() { return this.value; } - public void setValue(int newValue) { + public void setValue(BigDecimal newValue) { this.value = newValue; } public Sensor() { super("sensor"); } + + @Override + public String toString() { + return "Sensor{" + "value=" + value + ", sensor=" + sensor + '}'; + } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlug.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlug.java index e0d7981..fe936b3 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlug.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlug.java @@ -6,17 +6,19 @@ import javax.validation.constraints.NotNull; /** A smart plug that can be turned either on or off */ @Entity -public class SmartPlug extends OutputDevice { +public class SmartPlug extends Switchable { /** Whether the smart plug is on */ @Column(name = "smart_plug_on", nullable = false) @NotNull private boolean on; + @Override public boolean isOn() { return on; } + @Override public void setOn(boolean on) { this.on = on; } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlugRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlugRepository.java index 0b2fd344..08d145d 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlugRepository.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlugRepository.java @@ -1,3 +1,3 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; -public interface SmartPlugRepository extends DeviceRepository {} +public interface SmartPlugRepository extends SwitchableRepository {} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switch.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switch.java index 2576d38..d819dfe 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switch.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switch.java @@ -1,12 +1,18 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; +import java.util.HashSet; +import java.util.Set; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.OneToMany; /** A switch input device */ @Entity public class Switch extends InputDevice { + @OneToMany(mappedBy = "switchDevice") + private Set switchables = new HashSet<>(); + /** The state of this switch */ @Column(nullable = false, name = "switch_on") private boolean on; @@ -22,6 +28,15 @@ public class Switch extends InputDevice { */ public void setOn(boolean state) { on = state; + + for (final Switchable s : switchables) { + s.setOn(on); + } + } + + /** Toggle between on and off state */ + public void toggle() { + setOn(!isOn()); } /** @@ -32,4 +47,8 @@ public class Switch extends InputDevice { public boolean isOn() { return on; } + + public Set getOutputs() { + return switchables; + } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switchable.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switchable.java new file mode 100644 index 0000000..5ba0702 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switchable.java @@ -0,0 +1,47 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude; +import javax.persistence.*; + +/** A device that can be turned either on or off */ +@Entity +@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) +public abstract class Switchable extends OutputDevice { + + public static final Connector SWITCH_SWITCHABLE_CONNECTOR = + Connector.basic(Switch::getOutputs, Switchable::setSwitchId); + + @ManyToOne + @GsonExclude + @JoinColumn(name = "switch_id", updatable = false, insertable = false) + private Switch switchDevice; + + @Column(name = "switch_id") + private Long switchId; + + protected Switchable(String kind) { + super(kind); + } + + /** + * Returns whether the device is on (true) or not (false) + * + * @return whether the device is on (true) or not (false) + */ + public abstract boolean isOn(); + + /** + * Sets the on status of the device + * + * @param on the new on status: true for on, false for off + */ + public abstract void setOn(boolean on); + + public Long getSwitchId() { + return switchId; + } + + public void setSwitchId(Long switchId) { + this.switchId = switchId; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableRepository.java new file mode 100644 index 0000000..589542d --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableRepository.java @@ -0,0 +1,7 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +/** + * SwitchableRepository acts as a superclass for the other repositories so to mirror in the database + * the class inheritance present among the various switchable devices. + */ +public interface SwitchableRepository extends DeviceRepository {} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java index ba86712..dc6766d 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java @@ -1,6 +1,7 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; import io.swagger.annotations.ApiModelProperty; +import java.util.Objects; import javax.persistence.*; /** A user of the Smarthut application */ @@ -9,7 +10,7 @@ public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) - @Column(name = "id", updatable = false, nullable = false) + @Column(name = "id", updatable = false, nullable = false, unique = true) @ApiModelProperty(hidden = true) private Long id; @@ -17,8 +18,8 @@ public class User { @Column(nullable = false) private String name; - /** The full name of the user */ - @Column(nullable = false) + /** The full username of the user */ + @Column(nullable = false, unique = true) private String username; /** A properly salted way to store the password */ @@ -105,4 +106,22 @@ public class User { + isEnabled + '}'; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + User user = (User) o; + return id.equals(user.id) + && name.equals(user.name) + && username.equals(user.username) + && password.equals(user.password) + && email.equals(user.email) + && isEnabled.equals(user.isEnabled); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, username, password, email, isEnabled); + } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/scheduled/SensorUpdateTasks.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/scheduled/SensorUpdateTasks.java new file mode 100644 index 0000000..b1614aa --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/scheduled/SensorUpdateTasks.java @@ -0,0 +1,62 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.scheduled; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.controller.MotionSensorController; +import ch.usi.inf.sa4.sanmarinoes.smarthut.controller.SensorController; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.MotionSensorRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Sensor; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SensorRepository; +import java.math.BigDecimal; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.StreamSupport; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** Generates fake sensor (and motion sensor) updates as required by milestone one */ +@Component +public class SensorUpdateTasks { + + @Autowired private SensorRepository sensorRepository; + + @Autowired private MotionSensorRepository motionSensorRepository; + + @Autowired private SensorController sensorController; + + @Autowired private MotionSensorController motionSensorController; + + /** Generates fake sensor updates every two seconds with a +/- 1.25% error */ + @Scheduled(fixedRate = 2000) + public void sensorFakeUpdate() { + StreamSupport.stream(sensorRepository.findAll().spliterator(), true) + .forEach( + sensor -> + sensorController.updateValueFromSensor( + sensor, + Sensor.TYPICAL_VALUES + .get(sensor.getSensor()) + .multiply( + new BigDecimal( + 0.9875 + Math.random() / 40)))); + } + + /** + * Generate fake motion detections in all motion detectors every 20 seconds for 2 seconds at + * most + */ + @Scheduled(fixedDelay = 20000) + public void motionSensorFakeUpdate() { + StreamSupport.stream(motionSensorRepository.findAll().spliterator(), true) + .forEach( + sensor -> { + motionSensorController.updateDetectionFromMotionSensor(sensor, true); + CompletableFuture.delayedExecutor( + (long) (Math.random() * 2000), TimeUnit.MILLISECONDS) + .execute( + () -> + motionSensorController + .updateDetectionFromMotionSensor( + sensor, false)); + }); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/AuthenticationMessageListener.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/AuthenticationMessageListener.java new file mode 100644 index 0000000..89415aa --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/AuthenticationMessageListener.java @@ -0,0 +1,95 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.socket; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonConfig; +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtils; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import io.jsonwebtoken.ExpiredJwtException; +import java.io.IOException; +import java.util.Map; +import java.util.function.BiConsumer; +import javax.websocket.MessageHandler; +import javax.websocket.Session; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** Generates MessageHandlers for unauthenticated socket sessions */ +@Component +public class AuthenticationMessageListener { + + private Gson gson = GsonConfig.gson(); + + private JWTTokenUtils jwtTokenUtils; + + private UserRepository userRepository; + + @Autowired + public AuthenticationMessageListener( + JWTTokenUtils jwtTokenUtils, UserRepository userRepository) { + this.jwtTokenUtils = jwtTokenUtils; + this.userRepository = userRepository; + } + + /** + * Generates a new message handler to handle socket authentication + * + * @param session the session to which authentication must be checked + * @param authorizedSetter function to call once user is authenticated + * @return a new message handler to handle socket authentication + */ + MessageHandler.Whole newHandler( + final Session session, BiConsumer authorizedSetter) { + return new MessageHandler.Whole<>() { + @Override + public void onMessage(final String message) { + if (message == null) { + acknowledge(false); + return; + } + + String token; + String username; + + try { + token = gson.fromJson(message, JsonObject.class).get("token").getAsString(); + username = jwtTokenUtils.getUsernameFromToken(token); + } catch (ExpiredJwtException e) { + System.err.println(e.getMessage()); + acknowledge(false); + return; + } catch (Throwable ignored) { + System.out.println("Token format not valid"); + acknowledge(false); + return; + } + + final User user = userRepository.findByUsername(username); + if (user == null || jwtTokenUtils.isTokenExpired(token)) { + System.out.println("Token not valid"); + acknowledge(false); + return; + } + + // Here user is authenticated + session.removeMessageHandler(this); + + // Add user-session pair in authorized list + authorizedSetter.accept(user, session); + + // update client to acknowledge authentication + acknowledge(true); + } + + private void acknowledge(boolean success) { + try { + session.getBasicRemote() + .sendText(gson.toJson(Map.of("authenticated", success))); + } catch (IOException e) { + e.printStackTrace(); + } + } + }; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java new file mode 100644 index 0000000..503667a --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java @@ -0,0 +1,54 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.socket; + +import javax.websocket.server.ServerEndpointConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; +import org.springframework.web.socket.server.standard.ServerEndpointRegistration; + +/** Configures the sensor socket and maps it to the /sensor-socket path */ +@Configuration +public class SensorSocketConfig extends ServerEndpointConfig.Configurator { + + private SensorSocketEndpoint instance; + + @Autowired + public SensorSocketConfig(SensorSocketEndpoint instance) { + this.instance = instance; + } + + /** + * Registers the sensor socket endpoint to the url /sensor-socket + * + * @return an endpoint registration object + */ + @Bean + public ServerEndpointRegistration serverEndpointRegistration() { + return new ServerEndpointRegistration("/sensor-socket", instance); + } + + /** + * Returns a new ServerEndpointExporter + * + * @return a new ServerEndpointExporter + */ + @Bean + public ServerEndpointExporter endpointExporter() { + return new ServerEndpointExporter(); + } + + @Override + public T getEndpointInstance(Class endpointClass) throws InstantiationException { + try { + @SuppressWarnings("unchecked") + final T instance = (T) this.instance; + return instance; + } catch (ClassCastException e) { + final var e2 = + new InstantiationException("Cannot cast SensorSocketEndpoint to desired type"); + e2.initCause(e); + throw e2; + } + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java new file mode 100644 index 0000000..bc2f90e --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java @@ -0,0 +1,85 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.socket; + +import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.didThrow; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonConfig; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import com.google.gson.Gson; +import java.util.*; +import javax.websocket.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** Endpoint of socket at URL /sensor-socket used to update the client with sensor information */ +@Component +public class SensorSocketEndpoint extends Endpoint { + + private Gson gson = GsonConfig.gson(); + + private AuthenticationMessageListener authenticationMessageListener; + + private Set unauthorizedClients = Collections.synchronizedSet(new HashSet<>()); + + private Multimap authorizedClients = + Multimaps.synchronizedMultimap(HashMultimap.create()); + + @Autowired + public SensorSocketEndpoint(AuthenticationMessageListener authenticationMessageListener) { + this.authenticationMessageListener = authenticationMessageListener; + } + + /** + * Returns a synchronized set of socket sessions not yet authorized with a token + * + * @return a synchronized set of socket sessions not yet authorized with a token + */ + public Set getUnauthorizedClients() { + return unauthorizedClients; + } + + /** + * Returns a synchronized User to Session multimap with authorized sessions + * + * @return a synchronized User to Session multimap with authorized sessions + */ + public Multimap getAuthorizedClients() { + return authorizedClients; + } + + /** + * Given a message and a user, broadcasts that message in json to all associated clients and + * returns the number of successful transfers + * + * @param message the message to send + * @param u the user to which to send the message + * @return number of successful transfer + */ + public long broadcast(Object message, User u) { + final Collection sessions = authorizedClients.get(u); + return sessions.stream() + .parallel() + .filter(didThrow(s -> s.getBasicRemote().sendText(gson.toJson(message)))) + .count(); + } + + /** + * Handles the opening of a socket session with a client + * + * @param session the newly born session + * @param config endpoint configuration + */ + @Override + public void onOpen(Session session, EndpointConfig config) { + unauthorizedClients.add(session); + session.addMessageHandler( + authenticationMessageListener.newHandler( + session, + (u, s) -> { + unauthorizedClients.remove(s); + authorizedClients.put(u, s); + })); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/Utils.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/Utils.java index d9fcf12..99d363b 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/Utils.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/Utils.java @@ -1,14 +1,32 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.utils; import java.util.List; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.StreamSupport; /** A class with a bunch of useful static methods */ -public class Utils { +public final class Utils { private Utils() {} + @FunctionalInterface + public interface ConsumerWithException { + void apply(T input) throws Throwable; + } + public static List toList(Iterable iterable) { return StreamSupport.stream(iterable.spliterator(), false).collect(Collectors.toList()); } + + public static Predicate didThrow(ConsumerWithException consumer) { + return (t) -> { + try { + consumer.apply(t); + return true; + } catch (Throwable e) { + System.err.println(e.getMessage()); + return false; + } + }; + } } diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/AuthenticationTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/AuthenticationTests.java index 60761cd..d13104f 100644 --- a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/AuthenticationTests.java +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/AuthenticationTests.java @@ -8,6 +8,8 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.OkResponse; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserRegistrationRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateRegistrationException; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.UnauthorizedException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ConfirmationTokenRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import java.util.Map; @@ -25,6 +27,10 @@ public class AuthenticationTests extends SmartHutTest { @Autowired private TestRestTemplate restTemplate; + @Autowired private UserRepository userRepository; + + @Autowired private ConfirmationTokenRepository tokenRepository; + private UserRegistrationRequest getDisabledUser() { final UserRegistrationRequest disabledUser = new UserRegistrationRequest(); disabledUser.setName("Disabled User"); @@ -34,15 +40,6 @@ public class AuthenticationTests extends SmartHutTest { return disabledUser; } - private static final UserRegistrationRequest enabledUser = new UserRegistrationRequest(); - - static { - enabledUser.setName("Enabled User"); - enabledUser.setEmail("enabled@example.com"); - enabledUser.setUsername("enabled"); - enabledUser.setPassword("password"); - } - @Override protected void setUp() { final ResponseEntity res = @@ -50,12 +47,7 @@ public class AuthenticationTests extends SmartHutTest { this.url("/register"), getDisabledUser(), OkResponse.class); assertThat(res.getStatusCode().equals(HttpStatus.OK)); - final ResponseEntity res2 = - this.restTemplate.postForEntity( - this.url("/register"), enabledUser, OkResponse.class); - assertThat(res2.getStatusCode().equals(HttpStatus.OK)); - - // TODO: email confirmation for enabledUser + registerTestUser(restTemplate, userRepository, tokenRepository); } @Test @@ -230,4 +222,18 @@ public class AuthenticationTests extends SmartHutTest { assertThat(res.getBody() != null); assertThat(res.getBody().isUserDisabled()); } + + @Test + public void loginShouldReturnTokenWithEnabledUser() { + final JWTRequest request = new JWTRequest(); + request.setUsernameOrEmail("enabled"); + request.setPassword("password"); + + final ResponseEntity res = + this.restTemplate.postForEntity(url("/auth/login"), request, JWTResponse.class); + assertThat(res.getStatusCode().equals(HttpStatus.OK)); + assertThat(res.getBody() != null); + assertThat(res.getBody().getToken() != null); + assertThat(!res.getBody().getToken().isEmpty()); + } } diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmartHutTest.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmartHutTest.java index 5c6e097..f2b737a 100644 --- a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmartHutTest.java +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmartHutTest.java @@ -1,6 +1,18 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut; +import static org.assertj.core.api.Assertions.assertThat; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.OkResponse; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserRegistrationRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ConfirmationToken; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ConfirmationTokenRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository; import org.junit.jupiter.api.BeforeEach; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.reactive.function.client.WebClient; public abstract class SmartHutTest { private boolean setupDone = false; @@ -15,6 +27,38 @@ public abstract class SmartHutTest { protected void setUp() {} + protected static final UserRegistrationRequest enabledUser = new UserRegistrationRequest(); + + static { + enabledUser.setName("Enabled User"); + enabledUser.setEmail("enabled@example.com"); + enabledUser.setUsername("enabled"); + enabledUser.setPassword("password"); + } + + protected void registerTestUser( + final TestRestTemplate restTemplate, + final UserRepository userRepository, + final ConfirmationTokenRepository tokenRepository) { + final ResponseEntity res2 = + restTemplate.postForEntity(this.url("/register"), enabledUser, OkResponse.class); + assertThat(res2.getStatusCode().equals(HttpStatus.OK)); + + final User persistedEnabledUser = userRepository.findByUsername("enabled"); + final ConfirmationToken token = tokenRepository.findByUser(persistedEnabledUser); + + final ResponseEntity res3 = + WebClient.create(getBaseURL()) + .get() + .uri("/register/confirm-account?token=" + token.getConfirmationToken()) + .retrieve() + .toBodilessEntity() + .block(); + + assertThat(res3.getStatusCode().is2xxSuccessful()); + assertThat(userRepository.findByUsername("enabled").getEnabled()); + } + @BeforeEach void setUpHack() { if (!setupDone) { diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index ce6fe39..673d02a 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -28,7 +28,7 @@ server.port = 2000 email.registrationSubject=Complete your SmartHut.sm registration email.registration=To confirm your registration, please click here: -email.registrationPath=http://localhost:8080/register/confirm-account?token= +email.registrationPath=http://localhost:2000/register/confirm-account?token= email.resetpasswordSubject=SmartHut.sm password reset email.resetpassword=To reset your password, please click here: