Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Jacob Salvi 2020-05-08 14:58:32 +02:00
commit d4a0f195ad
29 changed files with 381 additions and 257 deletions

View file

@ -52,14 +52,10 @@ test:
reports:
junit: build/test-results/test/TEST-*.xml
#Runs a quality check on the code and creates a report on the codes
code_quality:
sonarqube:
image: gradle:jdk11
stage: code_quality
allow_failure: true
only:
- dev
script:
- gradle cpdCheck
artifacts:
paths:
- build/reports/cpd/cpdCheck.xml
#create a report on the quality of the code
expose_as: 'Code Quality Report'
- gradle build jacocoTestReport sonarqube -Dsonar.verbose=true -Dsonar.host.url=$SONAR_URL -Dsonar.login=$SONAR_LOGIN -Dsonar.projectKey=$CI_PROJECT_PATH_SLUG -Dsonar.projectName=$CI_PROJECT_PATH_SLUG -Dsonar.scm.disabled=True -Dsonar.coverage.jacoco.xmlReportPaths=./build/reports/jacoco/test/jacocoTestReport.xml

8
.mailmap Normal file
View file

@ -0,0 +1,8 @@
Claudio Maggioni <maggicl@usi.ch> Claudio Maggioni (maggicl) <maggicl@kolabnow.ch>
Claudio Maggioni <maggicl@usi.ch> Claudio Maggioni (maggicl) <maggicl@usi.ch>
Filippo Cesana <cesanf@usi.ch> FilippoCesana <cesanf@usi.ch>
Filippo Cesana <cesanf@usi.ch> Fil Cesana <cesanf@usi.ch>
Andrea Brites Marto <britea@usi.ch> britea <andreabritesma@gmail.com>
Christian Capeáns Pérez <capeac@usi.ch> christiancp <capeac@usi.ch>
Tommaso Rodolfo Masera <rodolt@usi.ch> tommi27 <tommi99@hotmail.it>
Matteo Omenetti <omenem@usi.ch> omenem <omenem@usi.ch>

View file

@ -1,8 +1,9 @@
plugins {
id 'org.springframework.boot' version '2.2.4.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id "de.aaschmid.cpd" version "3.1"
id 'java'
id 'jacoco'
id "org.sonarqube" version "2.8"
}
group = 'ch.usi.inf.sa4.sanmarinoes'
version = '0.0.1-SNAPSHOT'
@ -50,3 +51,13 @@ gradle.projectsEvaluated {
test {
useJUnitPlatform()
}
jacocoTestReport {
reports {
xml.enabled true
}
}
plugins.withType(JacocoPlugin) {
tasks["test"].finalizedBy 'jacocoTestReport'
}

4
gradle.properties Normal file
View file

@ -0,0 +1,4 @@
systemProp.sonar.host.url=https://lab.si.usi.ch:9000
systemProp.sonar.login=871fdfcb09345b1841f1730596ac32aacf3a86fb
systemProp.sonar.projectKey=SMASmarthutBackend
systemProp.sonar.scm.disabled=true

View file

@ -46,67 +46,67 @@ public class EmailConfigurationService {
@NotNull private String registrationRedirect;
public String getRegistrationSubject() {
public synchronized String getRegistrationSubject() {
return registrationSubject;
}
public void setRegistrationSubject(String registrationSubject) {
public synchronized void setRegistrationSubject(String registrationSubject) {
this.registrationSubject = registrationSubject;
}
public String getRegistration() {
public synchronized String getRegistration() {
return registration;
}
public void setRegistration(String registration) {
public synchronized void setRegistration(String registration) {
this.registration = registration;
}
public String getRegistrationPath() {
public synchronized String getRegistrationPath() {
return registrationPath;
}
public void setRegistrationPath(String registrationPath) {
public synchronized void setRegistrationPath(String registrationPath) {
this.registrationPath = registrationPath;
}
public String getResetPasswordSubject() {
public synchronized String getResetPasswordSubject() {
return resetPasswordSubject;
}
public void setResetPasswordSubject(String resetPasswordSubject) {
public synchronized void setResetPasswordSubject(String resetPasswordSubject) {
this.resetPasswordSubject = resetPasswordSubject;
}
public String getResetPassword() {
public synchronized String getResetPassword() {
return resetPassword;
}
public void setResetPassword(String resetPassword) {
public synchronized void setResetPassword(String resetPassword) {
this.resetPassword = resetPassword;
}
public String getResetPasswordPath() {
public synchronized String getResetPasswordPath() {
return resetPasswordPath;
}
public void setResetPasswordPath(String resetPasswordPath) {
public synchronized void setResetPasswordPath(String resetPasswordPath) {
this.resetPasswordPath = resetPasswordPath;
}
public String getResetPasswordRedirect() {
public synchronized String getResetPasswordRedirect() {
return resetPasswordRedirect;
}
public void setResetPasswordRedirect(String resetPasswordRedirect) {
public synchronized void setResetPasswordRedirect(String resetPasswordRedirect) {
this.resetPasswordRedirect = resetPasswordRedirect;
}
public String getRegistrationRedirect() {
public synchronized String getRegistrationRedirect() {
return registrationRedirect;
}
public void setRegistrationRedirect(String registrationRedirect) {
public synchronized void setRegistrationRedirect(String registrationRedirect) {
this.registrationRedirect = registrationRedirect;
}
}

View file

@ -233,9 +233,9 @@ public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
}
final Map<String, TypeAdapter<?>> labelToDelegate =
new LinkedHashMap<String, TypeAdapter<?>>();
new LinkedHashMap<String, TypeAdapter<?>>(labelToSubtype.size());
final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate =
new LinkedHashMap<Class<?>, TypeAdapter<?>>();
new LinkedHashMap<Class<?>, TypeAdapter<?>>(labelToSubtype.size());
for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
TypeAdapter<?> delegate =
gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
@ -245,7 +245,7 @@ public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
return new TypeAdapter<R>() {
@Override
public R read(JsonReader in) throws IOException {
public R read(JsonReader in) {
JsonElement jsonElement = Streams.parse(in);
JsonElement labelJsonElement;
if (maintainType) {

View file

@ -6,16 +6,14 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.JWTResponse;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.UnauthorizedException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.UserNotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
import ch.usi.inf.sa4.sanmarinoes.smarthut.service.JWTUserDetailsService;
import java.security.Principal;
import javax.validation.Valid;
import ch.usi.inf.sa4.sanmarinoes.smarthut.service.JWTUserDetailsService;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
@RestController
@ -30,8 +28,6 @@ public class AuthenticationController {
private final JWTUserDetailsService userDetailsService;
private BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
public AuthenticationController(
AuthenticationManager authenticationManager,
UserRepository userRepository,
@ -82,9 +78,9 @@ public class AuthenticationController {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password));
} catch (DisabledException e) {
throw new UnauthorizedException(true);
throw new UnauthorizedException(true, e);
} catch (BadCredentialsException e) {
throw new UnauthorizedException(false);
throw new UnauthorizedException(false, e);
}
}
}

View file

@ -33,6 +33,22 @@ public abstract class InputDeviceConnectionController<
this.input = input;
this.outputs = outputs;
}
public I getInput() {
return input;
}
public List<O> getOutputs() {
return outputs;
}
}
protected DeviceRepository<I> getInputRepository() {
return inputRepository;
}
protected DeviceRepository<O> getOutputReposiory() {
return outputReposiory;
}
@Autowired private DeviceService deviceService;
@ -65,7 +81,7 @@ public abstract class InputDeviceConnectionController<
inputRepository
.findByIdAndUsername(inputId, username)
.orElseThrow(() -> new NotFoundException("input device"));
final List<O> outputDevices = new ArrayList<>();
final List<O> outputDevices = new ArrayList<>(outputs.size());
for (final Long outputId : outputs) {
outputDevices.add(
outputReposiory
@ -87,12 +103,12 @@ public abstract class InputDeviceConnectionController<
Long inputId, List<Long> outputs, String username) throws NotFoundException {
final Connection pair = checkConnectionIDs(inputId, outputs, username);
for (final O o : pair.outputs) {
connector.connect(pair.input, o, true);
for (final O o : pair.getOutputs()) {
connector.connect(pair.getInput(), o, true);
}
deviceService.saveAllAsOwner(pair.outputs, username);
return pair.input.getOutputs();
deviceService.saveAllAsOwner(pair.getOutputs(), username);
return pair.getInput().getOutputs();
}
/**
@ -107,12 +123,12 @@ public abstract class InputDeviceConnectionController<
Long inputId, List<Long> outputs, String username) throws NotFoundException {
final Connection pair = checkConnectionIDs(inputId, outputs, username);
for (final O o : pair.outputs) {
connector.connect(pair.input, o, false);
for (final O o : pair.getOutputs()) {
connector.connect(pair.getInput(), o, false);
}
deviceService.saveAllAsOwner(pair.outputs, username);
return pair.input.getOutputs();
deviceService.saveAllAsOwner(pair.getOutputs(), username);
return pair.getInput().getOutputs();
}
@PostMapping("/{id}/lights")

View file

@ -17,14 +17,17 @@ import org.springframework.web.bind.annotation.*;
@RequestMapping("/knobDimmer")
public class KnobDimmerController extends InputDeviceConnectionController<KnobDimmer, Dimmable> {
@Autowired private DeviceService deviceService;
@Autowired private KnobDimmerRepository knobDimmerRepository;
private final DeviceService deviceService;
private final KnobDimmerRepository knobDimmerRepository;
@Autowired
protected KnobDimmerController(
KnobDimmerRepository inputRepository, DimmableRepository<Dimmable> outputRepository) {
KnobDimmerRepository inputRepository,
DimmableRepository<Dimmable> outputRepository,
DeviceService deviceService) {
super(inputRepository, outputRepository, Dimmable.KNOB_DIMMER_DIMMABLE_CONNECTOR);
this.knobDimmerRepository = inputRepository;
this.deviceService = deviceService;
}
@GetMapping("/{id}")

View file

@ -70,7 +70,7 @@ public class SceneController {
Utils.returnIfGuest(userRepository, null, hostId, principal);
return sceneService.applyAsGuest(
sceneRepository
.findByIdAndUserId(id, hostId)
.findByIdAndUserIdAndGuestAccessEnabled(id, hostId, true)
.orElseThrow(NotFoundException::new),
principal.getName(),
hostId);

View file

@ -18,7 +18,6 @@ import org.springframework.web.bind.annotation.*;
public class SwitchController extends InputDeviceConnectionController<Switch, Switchable> {
private SwitchRepository switchRepository;
private SwitchableRepository<Switchable> switchableRepository;
private DeviceService deviceService;
/**

View file

@ -36,8 +36,7 @@ public class ThermostatController {
newT = thermostatRepository.save(newT);
newT.setOn(t.isTurnOn());
newT = deviceService.saveAsOwner(newT, principal.getName());
return newT;
return deviceService.saveAsOwner(newT, principal.getName());
}
@PostMapping

View file

@ -2,7 +2,6 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
import ch.usi.inf.sa4.sanmarinoes.smarthut.config.EmailConfigurationService;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.InitPasswordResetRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.OkResponse;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.PasswordResetRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserRegistrationRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateRegistrationException;
@ -78,7 +77,7 @@ public class UserAccountController {
* @throws DuplicateRegistrationException if a user exists with same email or username
*/
@PostMapping
public OkResponse registerUser(@Valid @RequestBody UserRegistrationRequest registrationData)
public void registerUser(@Valid @RequestBody UserRegistrationRequest registrationData)
throws DuplicateRegistrationException {
final User existingEmailUser =
userRepository.findByEmailIgnoreCase(registrationData.getEmail());
@ -112,8 +111,6 @@ public class UserAccountController {
confirmationTokenRepository.save(token);
sendEmail(toSave.getEmail(), token, true);
return new OkResponse();
}
}
@ -125,7 +122,7 @@ public class UserAccountController {
* @throws UserNotFoundException if given email does not belong to any user
*/
@PostMapping("/init-reset-password")
public OkResponse initResetPassword(@Valid @RequestBody InitPasswordResetRequest resetRequest)
public void initResetPassword(@Valid @RequestBody InitPasswordResetRequest resetRequest)
throws UserNotFoundException {
final User toReset = userRepository.findByEmailIgnoreCase(resetRequest.getEmail());
@ -148,8 +145,6 @@ public class UserAccountController {
confirmationTokenRepository.save(token);
sendEmail(toReset.getEmail(), token, false);
return new OkResponse();
}
/**
@ -160,7 +155,7 @@ public class UserAccountController {
* @throws EmailTokenNotFoundException if given token is not a valid token for password reset
*/
@PutMapping("/reset-password")
public OkResponse resetPassword(
public void resetPassword(
@Valid @RequestBody PasswordResetRequest resetRequest,
final HttpServletResponse response)
throws EmailTokenNotFoundException, IOException {
@ -178,8 +173,6 @@ public class UserAccountController {
// Delete token to prevent further password changes
confirmationTokenRepository.delete(token);
return new OkResponse();
}
/**

View file

@ -1,6 +0,0 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
/** A dummy DTO to return when there is no data to return */
public class OkResponse {
private boolean success = true;
}

View file

@ -16,4 +16,16 @@ public class UserResponse {
us.username = u.getUsername();
return us;
}
public Long getId() {
return id;
}
public String getUsername() {
return username;
}
public String getName() {
return name;
}
}

View file

@ -7,8 +7,8 @@ import org.springframework.web.bind.annotation.ResponseStatus;
public class UnauthorizedException extends Exception {
private final boolean isUserDisabled;
public UnauthorizedException(boolean isDisabled) {
super("Access denied: " + (isDisabled ? "user is disabled" : "wrong credentials"));
public UnauthorizedException(boolean isDisabled, Throwable cause) {
super("Access denied: " + (isDisabled ? "user is disabled" : "wrong credentials"), cause);
this.isUserDisabled = isDisabled;
}

View file

@ -32,7 +32,7 @@ public class ConfirmationToken {
private User user;
@Column(nullable = false)
private Boolean resetPassword;
private boolean resetPassword;
public ConfirmationToken(User user) {
this.user = user;
@ -76,11 +76,11 @@ public class ConfirmationToken {
this.user = user;
}
public Boolean getResetPassword() {
public boolean getResetPassword() {
return resetPassword;
}
public void setResetPassword(Boolean resetPassword) {
public void setResetPassword(boolean resetPassword) {
this.resetPassword = resetPassword;
}
}

View file

@ -23,5 +23,6 @@ public interface SceneRepository extends CrudRepository<Scene, Long> {
@Query("SELECT s FROM Scene s JOIN s.user u WHERE u.id = ?1 AND s.guestAccessEnabled = true")
List<Scene> findByHostId(Long hostId);
Optional<Scene> findByIdAndUserId(Long id, Long userId);
Optional<Scene> findByIdAndUserIdAndGuestAccessEnabled(
Long id, Long userId, boolean guestAccessEnabled);
}

View file

@ -15,9 +15,9 @@ public class Sensor extends InputDevice implements RangeTriggerable {
public static final Map<SensorType, BigDecimal> TYPICAL_VALUES =
Map.of(
SensorType.TEMPERATURE, new BigDecimal(17.0),
SensorType.HUMIDITY, new BigDecimal(40.0),
SensorType.LIGHT, new BigDecimal(1000));
SensorType.TEMPERATURE, BigDecimal.valueOf(17.0),
SensorType.HUMIDITY, BigDecimal.valueOf(40.0),
SensorType.LIGHT, BigDecimal.valueOf(1000));
@Override
public double readTriggerState() {

View file

@ -6,11 +6,9 @@ import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Transient;
import javax.validation.constraints.NotNull;
import org.springframework.stereotype.Component;
/** A thermostat capable of controlling cooling and heating. */
@Entity
@Component
public class Thermostat extends Switchable implements BooleanTriggerable {
@Override

View file

@ -56,7 +56,7 @@ public class User {
@Column(nullable = false)
@GsonExclude
private Boolean isEnabled = false;
private boolean isEnabled = false;
public Long getId() {
return id;
@ -98,11 +98,11 @@ public class User {
this.password = password;
}
public Boolean getEnabled() {
public boolean getEnabled() {
return isEnabled;
}
public void setEnabled(Boolean enabled) {
public void setEnabled(boolean enabled) {
isEnabled = enabled;
}
@ -162,16 +162,17 @@ public class User {
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);
return cameraEnabled == user.cameraEnabled
&& isEnabled == user.isEnabled
&& Objects.equals(id, user.id)
&& Objects.equals(name, user.name)
&& Objects.equals(username, user.username)
&& Objects.equals(password, user.password)
&& Objects.equals(email, user.email);
}
@Override
public int hashCode() {
return Objects.hash(id, name, username, password, email, isEnabled);
return Objects.hash(id, name, username, password, email, isEnabled, cameraEnabled);
}
}

View file

@ -0,0 +1,20 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.service;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Device;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Thermostat;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class DevicePopulationService {
@Autowired private ThermostatService thermostatService;
public void populateComputedFields(Iterable<Device> devices) {
for (Device d : devices) {
if (d instanceof Thermostat) {
thermostatService.populateMeasuredTemperature((Thermostat) d);
}
}
}
}

View file

@ -0,0 +1,141 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.service;
import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
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.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class DevicePropagationService {
@Autowired private SensorSocketEndpoint endpoint;
@Autowired private EagerUserRepository userRepository;
@Autowired private DeviceRepository<Device> deviceRepository;
void propagateUpdateAsGuest(Device device, User host, User guest) {
final Set<User> guests = Set.copyOf(host.getGuests());
// We're telling the host that a guest has modified a device. Therefore, fromGuest becomes
// true.
// broadcast device update to host
endpoint.queueDeviceUpdate(device, host, true, null, false);
// We're telling all guests that a higher entity has issued a device update. Therefore,
// fromHost becomes true.
for (final User aGuest : guests) {
if (aGuest.equals(guest)) {
continue;
}
// enqueue all device updates for all other guests
endpoint.queueDeviceUpdate(device, aGuest, false, host.getId(), false);
}
}
void saveAllAsGuestSceneApplication(List<Device> devices, String guestUsername, Long hostId) {
final User guest = userRepository.findByUsername(guestUsername);
final User host = userRepository.findById(hostId).orElseThrow(IllegalStateException::new);
deviceRepository.saveAll(devices);
devices.forEach(d -> this.propagateUpdateAsGuest(d, host, guest));
}
void renameIfDuplicate(Device toCreate, String username) {
while (deviceRepository.findDuplicates(toCreate.getName(), username)
- (toCreate.getId() <= 0 ? 0 : 1)
> 0) {
toCreate.setName(toCreate.getName() + " (new)");
}
}
public <T extends Device> T saveAsGuest(T device, String guestUsername, Long hostId)
throws NotFoundException {
final User currentUser = userRepository.findByUsername(guestUsername);
final User host = userRepository.findById(hostId).orElseThrow(NotFoundException::new);
if (!host.getGuests().contains(currentUser)) throw new NotFoundException();
renameIfDuplicate(device, host.getUsername());
device = deviceRepository.save(device);
propagateUpdateAsGuest(device, host, currentUser);
return device;
}
/**
* Saves all the devices given in devices assuming that the owner updated them in one way or
* another. Takes care of the appropriate websocket updates and trigger checking as well. No
* checking is done to verify that the user whose username is given is in fact the owner of
* these devices
*
* @param devices the list of devices to save
* @param username the username of the owner of these devices
* @param fromScene true if the update comes from the a scene application side effect. Disables
* trigger checking to avoid recursive invocations of automations
* @param fromTrigger true if the update comes from a scene application executed by an
* automation. Propagates updated through socket to owner as well. No effect if fromScene is
* false.
* @param <T> the type of device contained in the list
* @return the updated list of devices, ready to be fed to GSON
*/
public <T extends Device> List<T> saveAllAsOwner(
Iterable<T> devices, String username, boolean fromScene, boolean fromTrigger) {
devices.forEach(d -> renameIfDuplicate(d, username));
devices = deviceRepository.saveAll(devices);
devices.forEach((d) -> propagateUpdateAsOwner(d, username, fromScene && fromTrigger));
return toList(devices);
}
public <T extends Device> List<T> saveAllAsOwner(Iterable<T> devices, String username) {
return saveAllAsOwner(devices, username, false, false);
}
public <T extends Device> T saveAsOwner(T device, String username) {
renameIfDuplicate(device, username);
device = deviceRepository.save(device);
propagateUpdateAsOwner(device, username, false);
return device;
}
public void deleteByIdAsOwner(Long id, String username) throws NotFoundException {
Device d =
deviceRepository
.findByIdAndUsername(id, username)
.orElseThrow(NotFoundException::new);
final User user = userRepository.findByUsername(username);
final Set<User> guests = user.getGuests();
// make sure we're broadcasting from host
for (final User guest : guests) {
// broadcast to endpoint the object device, with receiving user set to guest
endpoint.queueDeviceUpdate(d, guest, false, user.getId(), true);
}
deviceRepository.delete(d);
}
/**
* Propagates the update through the socket assuming that the user that modified the device is
* the owner of that device
*
* @param device the updated device
* @param username the username of the owner of that device
* @param causedByTrigger if true, send the update to the owner as well
*/
private void propagateUpdateAsOwner(Device device, String username, boolean causedByTrigger) {
final User user = userRepository.findByUsername(username);
final Set<User> guests = user.getGuests();
// make sure we're broadcasting from host
for (final User guest : guests) {
// broadcast to endpoint the object device, with receiving user set to guest
endpoint.queueDeviceUpdate(device, guest, false, user.getId(), false);
}
if (causedByTrigger) {
endpoint.queueDeviceUpdate(device, user, false, user.getId(), false);
}
}
}

View file

@ -4,11 +4,9 @@ import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
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.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.springframework.beans.factory.annotation.Autowired;
@ -17,28 +15,42 @@ import org.springframework.stereotype.Component;
@Component
public class DeviceService {
@Autowired private DeviceRepository<Device> deviceRepository;
@Autowired private AutomationRepository automationRepository;
@Autowired private SceneRepository sceneRepository;
@Autowired private SceneService sceneService;
@Autowired private TriggerRepository<Trigger<? extends Device>> triggerRepository;
@Autowired private RoomRepository roomRepository;
@Autowired private EagerUserRepository userRepository;
@Autowired private SensorSocketEndpoint endpoint;
@Autowired private ThermostatService thermostatService;
private final DeviceRepository<Device> deviceRepository;
private final AutomationRepository automationRepository;
private final SceneRepository sceneRepository;
private final SceneService sceneService;
private final TriggerRepository<Trigger<? extends Device>> triggerRepository;
private final RoomRepository roomRepository;
private final EagerUserRepository userRepository;
private final DevicePopulationService devicePopulationService;
private final DevicePropagationService devicePropagationService;
@Autowired
public DeviceService(
DeviceRepository<Device> deviceRepository,
AutomationRepository automationRepository,
SceneRepository sceneRepository,
SceneService sceneService,
TriggerRepository<Trigger<? extends Device>> triggerRepository,
RoomRepository roomRepository,
EagerUserRepository userRepository,
DevicePopulationService devicePopulationService,
DevicePropagationService devicePropagationService) {
this.deviceRepository = deviceRepository;
this.automationRepository = automationRepository;
this.sceneRepository = sceneRepository;
this.sceneService = sceneService;
this.triggerRepository = triggerRepository;
this.roomRepository = roomRepository;
this.userRepository = userRepository;
this.devicePopulationService = devicePopulationService;
this.devicePropagationService = devicePropagationService;
}
public void throwIfRoomNotOwned(Long roomId, String username) throws NotFoundException {
roomRepository.findByIdAndUsername(roomId, username).orElseThrow(NotFoundException::new);
}
private void renameIfDuplicate(Device toCreate, String username) {
while (deviceRepository.findDuplicates(toCreate.getName(), username)
- (toCreate.getId() <= 0 ? 0 : 1)
> 0) {
toCreate.setName(toCreate.getName() + " (new)");
}
}
private void triggerTriggers(Device device, final String username) {
final long deviceId = device.getId();
@ -66,9 +78,64 @@ public class DeviceService {
return findAll(null, hostId, username);
}
public <T extends Device> T saveAsGuest(T device, String guestUsername, Long hostId)
throws NotFoundException {
final User currentUser = userRepository.findByUsername(guestUsername);
final User host = userRepository.findById(hostId).orElseThrow(NotFoundException::new);
if (!host.getGuests().contains(currentUser)) throw new NotFoundException();
devicePropagationService.renameIfDuplicate(device, host.getUsername());
device = deviceRepository.save(device);
devicePropagationService.propagateUpdateAsGuest(device, host, currentUser);
return device;
}
public void deleteByIdAsOwner(Long id, String username) throws NotFoundException {
devicePropagationService.deleteByIdAsOwner(id, username);
}
public void populateComputedFields(Iterable<Device> devices) {
devicePopulationService.populateComputedFields(devices);
}
/**
* Saves all the devices given in devices assuming that the owner updated them in one way or
* another. Takes care of the appropriate websocket updates and trigger checking as well. No
* checking is done to verify that the user whose username is given is in fact the owner of
* these devices
*
* @param devices the list of devices to save
* @param username the username of the owner of these devices
* @param fromScene true if the update comes from the a scene application side effect. Disables
* trigger checking to avoid recursive invocations of automations
* @param fromTrigger true if the update comes from a scene application executed by an
* automation. Propagates updated through socket to owner as well. No effect if fromScene is
* false.
* @param <T> the type of device contained in the list
* @return the updated list of devices, ready to be fed to GSON
*/
public <T extends Device> List<T> saveAllAsOwner(
Iterable<T> devices, String username, boolean fromScene, boolean fromTrigger) {
List<T> toReturn =
devicePropagationService.saveAllAsOwner(devices, username, fromScene, fromTrigger);
if (!fromScene) {
toReturn.forEach((d) -> this.triggerTriggers(d, username));
}
return toReturn;
}
public <T extends Device> List<T> saveAllAsOwner(Iterable<T> devices, String username) {
return saveAllAsOwner(devices, username, false, false);
}
public <T extends Device> T saveAsOwner(T device, String username) {
T toReturn = devicePropagationService.saveAsOwner(device, username);
triggerTriggers(toReturn, username);
return device;
}
public List<Device> findAll(Long roomId, Long hostId, String username)
throws NotFoundException {
try {
Iterable<Device> devices;
User host = null;
if (hostId == null) {
@ -99,7 +166,7 @@ public class DeviceService {
}
}
populateComputedFields(devices);
devicePopulationService.populateComputedFields(devices);
if (host != null && !host.isCameraEnabled()) {
return StreamSupport.stream(devices.spliterator(), true)
@ -108,139 +175,5 @@ public class DeviceService {
} else {
return toList(devices);
}
} catch (NotFoundException e) {
e.printStackTrace();
throw e;
}
}
public void populateComputedFields(Iterable<Device> devices) {
for (Device d : devices) {
if (d instanceof Thermostat) {
thermostatService.populateMeasuredTemperature((Thermostat) d);
}
}
}
public <T extends Device> T saveAsGuest(T device, String guestUsername, Long hostId)
throws NotFoundException {
final User currentUser = userRepository.findByUsername(guestUsername);
final User host = userRepository.findById(hostId).orElseThrow(NotFoundException::new);
if (!host.getGuests().contains(currentUser)) throw new NotFoundException();
renameIfDuplicate(device, host.getUsername());
device = deviceRepository.save(device);
propagateUpdateAsGuest(device, host, currentUser);
return device;
}
private void propagateUpdateAsGuest(Device device, User host, User guest) {
final Set<User> guests = Set.copyOf(host.getGuests());
// We're telling the host that a guest has modified a device. Therefore, fromGuest becomes
// true.
// broadcast device update to host
endpoint.queueDeviceUpdate(device, host, true, null, false);
// We're telling all guests that a higher entity has issued a device update. Therefore,
// fromHost becomes true.
for (final User aGuest : guests) {
if (aGuest.equals(guest)) {
continue;
}
// enqueue all device updates for all other guests
endpoint.queueDeviceUpdate(device, aGuest, false, host.getId(), false);
}
}
List<Device> saveAllAsGuestSceneApplication(
List<Device> devices, String guestUsername, Long hostId) {
final User guest = userRepository.findByUsername(guestUsername);
final User host = userRepository.findById(hostId).orElseThrow(IllegalStateException::new);
deviceRepository.saveAll(devices);
devices.forEach(d -> this.propagateUpdateAsGuest(d, host, guest));
return devices;
}
/**
* Propagates the update through the socket assuming that the user that modified the device is
* the owner of that device
*
* @param device the updated device
* @param username the username of the owner of that device
* @param causedByTrigger if true, send the update to the owner as well
*/
private void propagateUpdateAsOwner(Device device, String username, boolean causedByTrigger) {
final User user = userRepository.findByUsername(username);
final Set<User> guests = user.getGuests();
// make sure we're broadcasting from host
for (final User guest : guests) {
// broadcast to endpoint the object device, with receiving user set to guest
endpoint.queueDeviceUpdate(device, guest, false, user.getId(), false);
}
if (causedByTrigger) {
endpoint.queueDeviceUpdate(device, user, false, user.getId(), false);
}
}
/**
* Saves all the devices given in devices assuming that the owner updated them in one way or
* another. Takes care of the appropriate websocket updates and trigger checking as well. No
* checking is done to verify that the user whose username is given is in fact the owner of
* these devices
*
* @param devices the list of devices to save
* @param username the username of the owner of these devices
* @param fromScene true if the update comes from the a scene application side effect. Disables
* trigger checking to avoid recursive invocations of automations
* @param fromTrigger true if the update comes from a scene application executed by an
* automation. Propagates updated through socket to owner as well. No effect if fromScene is
* false.
* @param <T> the type of device contained in the list
* @return the updated list of devices, ready to be fed to GSON
*/
public <T extends Device> List<T> saveAllAsOwner(
Iterable<T> devices, String username, boolean fromScene, boolean fromTrigger) {
devices.forEach(d -> renameIfDuplicate(d, username));
devices = deviceRepository.saveAll(devices);
devices.forEach((d) -> propagateUpdateAsOwner(d, username, fromScene && fromTrigger));
if (!fromScene) {
devices.forEach((d) -> triggerTriggers(d, username));
}
return toList(devices);
}
public <T extends Device> List<T> saveAllAsOwner(Iterable<T> devices, String username) {
return saveAllAsOwner(devices, username, false, false);
}
public <T extends Device> T saveAsOwner(T device, String username) {
renameIfDuplicate(device, username);
device = deviceRepository.save(device);
propagateUpdateAsOwner(device, username, false);
triggerTriggers(device, username);
return device;
}
public void deleteByIdAsOwner(Long id, String username) throws NotFoundException {
Device d =
deviceRepository
.findByIdAndUsername(id, username)
.orElseThrow(NotFoundException::new);
final User user = userRepository.findByUsername(username);
final Set<User> guests = user.getGuests();
// make sure we're broadcasting from host
for (final User guest : guests) {
// broadcast to endpoint the object device, with receiving user set to guest
endpoint.queueDeviceUpdate(d, guest, false, user.getId(), true);
}
deviceRepository.delete(d);
}
}

View file

@ -10,30 +10,31 @@ import org.springframework.stereotype.Component;
public class SceneService {
@Autowired private DeviceRepository<Device> deviceRepository;
@Autowired private DeviceService deviceService;
@Autowired private DevicePopulationService devicePopulationService;
@Autowired private DevicePropagationService devicePropagationService;
@Autowired private StateRepository<State<?>> stateRepository;
private List<Device> copyStatesToDevices(Scene fromScene) {
final List<Device> updated = new ArrayList<>();
final List<Device> updated = new ArrayList<>(fromScene.getStates().size());
for (final State<?> s : fromScene.getStates()) {
s.apply();
updated.add(s.getDevice());
}
deviceService.populateComputedFields(updated);
devicePopulationService.populateComputedFields(updated);
return updated;
}
public List<Device> apply(Scene newScene, String username, boolean fromTrigger) {
List<Device> updated = copyStatesToDevices(newScene);
deviceService.saveAllAsOwner(updated, username, true, fromTrigger);
devicePropagationService.saveAllAsOwner(updated, username, true, fromTrigger);
return updated;
}
public List<Device> applyAsGuest(Scene newScene, String username, Long hostId) {
List<Device> updated = copyStatesToDevices(newScene);
deviceService.saveAllAsGuestSceneApplication(updated, username, hostId);
devicePropagationService.saveAllAsGuestSceneApplication(updated, username, hostId);
return updated;
}

View file

@ -16,7 +16,7 @@ public class ThermostatService {
@Autowired private SensorSocketEndpoint endpoint;
@Autowired private DeviceService deviceService;
@Autowired private DevicePropagationService deviceService;
@Autowired private ThermostatRepository thermostatRepository;

View file

@ -5,7 +5,7 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtils;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Device;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository;
import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService;
import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DevicePopulationService;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
@ -20,15 +20,15 @@ import org.springframework.stereotype.Component;
@Component
public class SensorSocketEndpoint extends Endpoint {
private Gson gson = GsonConfig.socketGson();
private final Gson gson = GsonConfig.socketGson();
@Autowired private DeviceService deviceService;
@Autowired private DevicePopulationService deviceService;
private UserRepository userRepository;
private final UserRepository userRepository;
private JWTTokenUtils jwtTokenUtils;
private final JWTTokenUtils jwtTokenUtils;
private Multimap<User, Session> authorizedClients =
private final Multimap<User, Session> authorizedClients =
Multimaps.synchronizedMultimap(HashMultimap.create());
// messages are now stored as strings as a "hack" to capture and clone the state of the device,

View file

@ -4,7 +4,6 @@ import static org.assertj.core.api.Assertions.assertThat;
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.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;
@ -42,9 +41,9 @@ public class AuthenticationTests extends SmartHutTest {
@Override
protected void setUp() {
final ResponseEntity<OkResponse> res =
final ResponseEntity<Object> res =
this.restTemplate.postForEntity(
this.url("/register"), getDisabledUser(), OkResponse.class);
this.url("/register"), getDisabledUser(), Object.class);
assertThat(res.getStatusCode().equals(HttpStatus.OK));
registerTestUser(restTemplate, userRepository, tokenRepository);
@ -178,8 +177,8 @@ public class AuthenticationTests extends SmartHutTest {
request.setEmail("smarthut.sm@example.com");
request.setPassword("password");
final ResponseEntity<OkResponse> res =
this.restTemplate.postForEntity(url("/register"), request, OkResponse.class);
final ResponseEntity<Object> res =
this.restTemplate.postForEntity(url("/register"), request, Object.class);
assertThat(res.getStatusCode().equals(HttpStatus.OK));
assertThat(res.getBody() != null);
}

View file

@ -2,7 +2,6 @@ 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;
@ -40,8 +39,8 @@ public abstract class SmartHutTest {
final TestRestTemplate restTemplate,
final UserRepository userRepository,
final ConfirmationTokenRepository tokenRepository) {
final ResponseEntity<OkResponse> res2 =
restTemplate.postForEntity(this.url("/register"), enabledUser, OkResponse.class);
final ResponseEntity<Object> res2 =
restTemplate.postForEntity(this.url("/register"), enabledUser, Object.class);
assertThat(res2.getStatusCode().equals(HttpStatus.OK));
final User persistedEnabledUser = userRepository.findByUsername("enabled");