diff --git a/build.gradle b/build.gradle index 3116923..c55c8c1 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' repositories { mavenCentral() + } dependencies { compile 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final' @@ -39,6 +40,13 @@ dependencies { // Fixes https://stackoverflow.com/a/60455550 testImplementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.11' } + +gradle.projectsEvaluated { + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" + } +} + test { useJUnitPlatform() } \ No newline at end of file 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 69c4fc9..67b25dc 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 @@ -1,5 +1,8 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.config; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableState; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.State; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SwitchableState; import com.google.gson.*; import java.lang.reflect.Type; import org.springframework.context.annotation.Bean; @@ -24,6 +27,11 @@ public class GsonConfig { final GsonBuilder builder = new GsonBuilder(); builder.registerTypeAdapter(Json.class, new SpringfoxJsonToGsonAdapter()); builder.addSerializationExclusionStrategy(new AnnotationExclusionStrategy()); + RuntimeTypeAdapterFactory runtimeTypeAdapterFactory = + RuntimeTypeAdapterFactory.of(State.class, "kind") + .registerSubtype(SwitchableState.class, "switchableState") + .registerSubtype(DimmableState.class, "dimmableState"); + builder.registerTypeAdapterFactory(runtimeTypeAdapterFactory); return builder.create(); } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/RuntimeTypeAdapterFactory.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/RuntimeTypeAdapterFactory.java new file mode 100644 index 0000000..e4a9107 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/RuntimeTypeAdapterFactory.java @@ -0,0 +1,315 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.config; + +/* + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.Streams; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Adapts values whose runtime type may differ from their declaration type. This is necessary when a + * field's type is not the same type that GSON should create when deserializing that field. For + * example, consider these types: + * + *
{@code
+ * abstract class Shape {
+ *   int x;
+ *   int y;
+ * }
+ * class Circle extends Shape {
+ *   int radius;
+ * }
+ * class Rectangle extends Shape {
+ *   int width;
+ *   int height;
+ * }
+ * class Diamond extends Shape {
+ *   int width;
+ *   int height;
+ * }
+ * class Drawing {
+ *   Shape bottomShape;
+ *   Shape topShape;
+ * }
+ * }
+ * + *

Without additional type information, the serialized JSON is ambiguous. Is the bottom shape in + * this drawing a rectangle or a diamond? + * + *

{@code
+ * {
+ *   "bottomShape": {
+ *     "width": 10,
+ *     "height": 5,
+ *     "x": 0,
+ *     "y": 0
+ *   },
+ *   "topShape": {
+ *     "radius": 2,
+ *     "x": 4,
+ *     "y": 1
+ *   }
+ * }
+ * }
+ * + * This class addresses this problem by adding type information to the serialized JSON and honoring + * that type information when the JSON is deserialized: + * + *
{@code
+ * {
+ *   "bottomShape": {
+ *     "type": "Diamond",
+ *     "width": 10,
+ *     "height": 5,
+ *     "x": 0,
+ *     "y": 0
+ *   },
+ *   "topShape": {
+ *     "type": "Circle",
+ *     "radius": 2,
+ *     "x": 4,
+ *     "y": 1
+ *   }
+ * }
+ * }
+ * + * Both the type field name ({@code "type"}) and the type labels ({@code "Rectangle"}) are + * configurable. + * + *

Registering Types

+ * + * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field name to the + * {@link #of} factory method. If you don't supply an explicit type field name, {@code "type"} will + * be used. + * + *
{@code
+ * RuntimeTypeAdapterFactory shapeAdapterFactory
+ *     = RuntimeTypeAdapterFactory.of(Shape.class, "type");
+ * }
+ * + * Next register all of your subtypes. Every subtype must be explicitly registered. This protects + * your application from injection attacks. If you don't supply an explicit type label, the type's + * simple name will be used. + * + *
{@code
+ * shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
+ * shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
+ * shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
+ * }
+ * + * Finally, register the type adapter factory in your application's GSON builder: + * + *
{@code
+ * Gson gson = new GsonBuilder()
+ *     .registerTypeAdapterFactory(shapeAdapterFactory)
+ *     .create();
+ * }
+ * + * Like {@code GsonBuilder}, this API supports chaining: + * + *
{@code
+ * RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
+ *     .registerSubtype(Rectangle.class)
+ *     .registerSubtype(Circle.class)
+ *     .registerSubtype(Diamond.class);
+ * }
+ * + *

Serialization and deserialization

+ * + * In order to serialize and deserialize a polymorphic object, you must specify the base type + * explicitly. + * + *
{@code
+ * Diamond diamond = new Diamond();
+ * String json = gson.toJson(diamond, Shape.class);
+ * }
+ * + * And then: + * + *
{@code
+ * Shape shape = gson.fromJson(json, Shape.class);
+ * }
+ */ +public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { + private final Class baseType; + private final String typeFieldName; + private final Map> labelToSubtype = new LinkedHashMap>(); + private final Map, String> subtypeToLabel = new LinkedHashMap, String>(); + private final boolean maintainType; + + private RuntimeTypeAdapterFactory( + Class baseType, String typeFieldName, boolean maintainType) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); + } + this.baseType = baseType; + this.typeFieldName = typeFieldName; + this.maintainType = maintainType; + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code typeFieldName} as + * the type field name. Type field names are case sensitive. {@code maintainType} flag decide if + * the type will be stored in pojo or not. + */ + public static RuntimeTypeAdapterFactory of( + Class baseType, String typeFieldName, boolean maintainType) { + return new RuntimeTypeAdapterFactory(baseType, typeFieldName, maintainType); + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code typeFieldName} as + * the type field name. Type field names are case sensitive. + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory(baseType, typeFieldName, false); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as the type + * field name. + */ + public static RuntimeTypeAdapterFactory of(Class baseType) { + return new RuntimeTypeAdapterFactory(baseType, "type", false); + } + + /** + * Registers {@code type} identified by {@code label}. Labels are case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or {@code label} have already been + * registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { + if (type == null || label == null) { + throw new NullPointerException(); + } + if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { + throw new IllegalArgumentException("types and labels must be unique"); + } + labelToSubtype.put(label, type); + subtypeToLabel.put(type, label); + return this; + } + + /** + * Registers {@code type} identified by its {@link Class#getSimpleName simple name}. Labels are + * case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or its simple name have already been + * registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type) { + return registerSubtype(type, type.getSimpleName()); + } + + public TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() != baseType) { + return null; + } + + final Map> labelToDelegate = + new LinkedHashMap>(); + final Map, TypeAdapter> subtypeToDelegate = + new LinkedHashMap, TypeAdapter>(); + for (Map.Entry> entry : labelToSubtype.entrySet()) { + TypeAdapter delegate = + gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); + } + + return new TypeAdapter() { + @Override + public R read(JsonReader in) throws IOException { + JsonElement jsonElement = Streams.parse(in); + JsonElement labelJsonElement; + if (maintainType) { + labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); + } else { + labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); + } + + if (labelJsonElement == null) { + throw new JsonParseException( + "cannot deserialize " + + baseType + + " because it does not define a field named " + + typeFieldName); + } + String label = labelJsonElement.getAsString(); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException( + "cannot deserialize " + + baseType + + " subtype named " + + label + + "; did you forget to register a subtype?"); + } + return delegate.fromJsonTree(jsonElement); + } + + @Override + public void write(JsonWriter out, R value) throws IOException { + Class srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); + if (delegate == null) { + throw new JsonParseException( + "cannot serialize " + + srcType.getName() + + "; did you forget to register a subtype?"); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + + if (maintainType) { + Streams.write(jsonObject, out); + return; + } + + JsonObject clone = new JsonObject(); + + if (jsonObject.has(typeFieldName)) { + throw new JsonParseException( + "cannot serialize " + + srcType.getName() + + " because it already defines a field named " + + typeFieldName); + } + clone.add(typeFieldName, new JsonPrimitive(label)); + + for (Map.Entry e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + Streams.write(clone, out); + } + }.nullSafe(); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/CurtainsController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/CurtainsController.java index 3ad0974..5eb00be 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/CurtainsController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/CurtainsController.java @@ -3,13 +3,9 @@ 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.DimmableSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Curtains; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.CurtainsRepository; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Dimmable; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableState; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SceneRepository; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.StateRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import java.security.Principal; import java.util.List; import javax.validation.Valid; @@ -23,7 +19,7 @@ import org.springframework.web.bind.annotation.*; public class CurtainsController { @Autowired private CurtainsRepository curtainsService; @Autowired private SceneRepository sceneRepository; - @Autowired private StateRepository stateRepository; + @Autowired private StateRepository> stateRepository; @GetMapping public List findAll() { @@ -62,19 +58,21 @@ public class CurtainsController { } @PostMapping("/{id}/state") - public void sceneBinding( + public State sceneBinding( @PathVariable("id") long deviceId, @RequestParam long sceneId, final Principal principal) - throws NotFoundException { + throws NotFoundException, DuplicateStateException { Curtains c = curtainsService .findByIdAndUsername(deviceId, principal.getName()) .orElseThrow(NotFoundException::new); - DimmableState s = c.cloneState(); + State s = c.cloneState(); sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new); s.setSceneId(sceneId); - stateRepository.save(s); + if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0) + throw new DuplicateStateException(); + return stateRepository.save(s); } } 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 367dca3..cb7cc00 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 @@ -3,13 +3,9 @@ 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.DimmableSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Dimmable; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLight; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLightRepository; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SceneRepository; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.State; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.StateRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import java.security.Principal; import java.util.List; import javax.validation.Valid; @@ -24,7 +20,7 @@ public class DimmableLightController { @Autowired private DimmableLightRepository dimmableLightService; @Autowired private SceneRepository sceneRepository; - @Autowired private StateRepository stateRepository; + @Autowired private StateRepository> stateRepository; @GetMapping public List findAll() { @@ -68,19 +64,21 @@ public class DimmableLightController { // the full url should be: "/dimmableLight/{id}/state?sceneId={sceneId} // however it is not necessary to specify the query in the mapping @PostMapping("/{id}/state") - public void sceneBinding( + public State sceneBinding( @PathVariable("id") long deviceId, @RequestParam long sceneId, final Principal principal) - throws NotFoundException { + throws NotFoundException, DuplicateStateException { DimmableLight d = dimmableLightService .findByIdAndUsername(deviceId, principal.getName()) .orElseThrow(NotFoundException::new); - State s = d.cloneState(); + State s = d.cloneState(); sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new); s.setSceneId(sceneId); - stateRepository.save(s); + if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0) + throw new DuplicateStateException(); + return stateRepository.save(s); } } 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 cc0a5ee..ac0fd47 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 @@ -3,13 +3,9 @@ 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.SwitchableSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.OutputDevice; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RegularLight; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RegularLightRepository; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SceneRepository; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.State; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.StateRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import java.security.Principal; import java.util.List; import javax.validation.Valid; @@ -32,7 +28,7 @@ public class RegularLightController { @Autowired private RegularLightRepository regularLightService; @Autowired private SceneRepository sceneRepository; - @Autowired private StateRepository stateRepository; + @Autowired private StateRepository> stateRepository; @GetMapping public List findAll() { @@ -76,18 +72,20 @@ public class RegularLightController { // the full url should be: "/dimmableLight/{id}/state?sceneId={sceneId} // however it is not necessary to specify the query in the mapping @PostMapping("/{id}/state") - public void sceneBinding( + public State sceneBinding( @PathVariable("id") long deviceId, @RequestParam long sceneId, final Principal principal) - throws NotFoundException { + throws NotFoundException, DuplicateStateException { RegularLight d = regularLightService .findByIdAndUsername(deviceId, principal.getName()) .orElseThrow(NotFoundException::new); - State s = d.cloneState(); + State s = d.cloneState(); sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new); s.setSceneId(sceneId); - stateRepository.save(s); + if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0) + throw new DuplicateStateException(); + return stateRepository.save(s); } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SecurityCameraController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SecurityCameraController.java index bd5bd23..d7fdc8e 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SecurityCameraController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SecurityCameraController.java @@ -3,13 +3,9 @@ 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.SwitchableSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.OutputDevice; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SceneRepository; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SecurityCamera; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SecurityCameraRepository; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.State; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.StateRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import java.security.Principal; import java.util.List; import javax.validation.Valid; @@ -32,7 +28,7 @@ public class SecurityCameraController { @Autowired SecurityCameraRepository securityCameraService; @Autowired private SceneRepository sceneRepository; - @Autowired private StateRepository stateRepository; + @Autowired private StateRepository> stateRepository; @GetMapping public List findAll() { @@ -74,19 +70,21 @@ public class SecurityCameraController { } @PostMapping("/{id}/state") - public void sceneBinding( + public State sceneBinding( @PathVariable("id") long deviceId, @RequestParam long sceneId, final Principal principal) - throws NotFoundException { + throws NotFoundException, DuplicateStateException { SecurityCamera d = securityCameraService .findByIdAndUsername(deviceId, principal.getName()) .orElseThrow(NotFoundException::new); - State s = d.cloneState(); + State s = d.cloneState(); sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new); s.setSceneId(sceneId); - stateRepository.save(s); + if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0) + throw new DuplicateStateException(); + return stateRepository.save(s); } } 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 1a165d8..d90c9ac 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 @@ -3,6 +3,7 @@ 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.SwitchableSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import java.security.Principal; @@ -19,7 +20,7 @@ public class SmartPlugController { @Autowired private SmartPlugRepository smartPlugRepository; @Autowired private SceneRepository sceneRepository; - @Autowired private StateRepository stateRepository; + @Autowired private StateRepository> stateRepository; @GetMapping public List findAll() { @@ -73,19 +74,21 @@ public class SmartPlugController { } @PostMapping("/{id}/state") - public void sceneBinding( + public State sceneBinding( @PathVariable("id") long deviceId, @RequestParam long sceneId, final Principal principal) - throws NotFoundException { + throws NotFoundException, DuplicateStateException { SmartPlug d = smartPlugRepository .findByIdAndUsername(deviceId, principal.getName()) .orElseThrow(NotFoundException::new); - State s = d.cloneState(); + State s = d.cloneState(); sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new); s.setSceneId(sceneId); - stateRepository.save(s); + if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0) + throw new DuplicateStateException(); + return stateRepository.save(s); } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatController.java index 8a3137c..ee566ed 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatController.java @@ -1,6 +1,7 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.ThermostatSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import ch.usi.inf.sa4.sanmarinoes.smarthut.service.ThermostatService; @@ -21,7 +22,7 @@ public class ThermostatController { @Autowired private ThermostatService thermostatService; @Autowired private SceneRepository sceneRepository; - @Autowired private StateRepository stateRepository; + @Autowired private StateRepository> stateRepository; @GetMapping public List findAll(Principal user) { @@ -70,19 +71,21 @@ public class ThermostatController { } @PostMapping("/{id}/state") - public void sceneBinding( + public State sceneBinding( @PathVariable("id") long deviceId, @RequestParam long sceneId, final Principal principal) - throws NotFoundException { + throws NotFoundException, DuplicateStateException { Thermostat d = thermostatRepository .findByIdAndUsername(deviceId, principal.getName()) .orElseThrow(NotFoundException::new); - State s = d.cloneState(); + State s = d.cloneState(); sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new); s.setSceneId(sceneId); - stateRepository.save(s); + if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0) + throw new DuplicateStateException(); + return stateRepository.save(s); } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/DuplicateStateException.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/DuplicateStateException.java new file mode 100644 index 0000000..4e9f3d5 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/DuplicateStateException.java @@ -0,0 +1,12 @@ +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 DuplicateStateException extends Exception { + public DuplicateStateException() { + super( + "Cannot create state since it has already been created for this scene and this device"); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmable.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmable.java index 4bfd828..a3ae206 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmable.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmable.java @@ -78,7 +78,7 @@ public class Dimmable extends Switchable { } @Override - public DimmableState cloneState() { + public State cloneState() { final DimmableState newState = new DimmableState<>(); newState.setDeviceId(getId()); newState.setDevice(this); diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableState.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableState.java index 1f548f8..7c0b8d2 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableState.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableState.java @@ -1,28 +1,24 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; -import javax.persistence.Column; import javax.persistence.Entity; import javax.validation.constraints.Max; import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; /** Represent a state for an IDimmable device */ @Entity public class DimmableState extends State { /** The light intensity value. Goes from 0 (off) to 100 (on) */ - @NotNull - @Column(nullable = false) @Min(0) @Max(100) - private Integer dimAmount = 0; + private int intensity = 0; - public Integer getIntensity() { - return dimAmount; + public int getIntensity() { + return intensity; } - public void setIntensity(Integer dimAmount) { - this.dimAmount = dimAmount; + public void setIntensity(int dimAmount) { + this.intensity = dimAmount; } @Override 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 34f3824..4f0f592 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 @@ -145,6 +145,7 @@ public class Room { */ @NotNull @Column(name = "user_id", nullable = false) + @GsonExclude private Long userId; /** The user given name of this room (e.g. 'Master bedroom') */ diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Scene.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Scene.java index 3d026b7..e762087 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Scene.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Scene.java @@ -31,6 +31,7 @@ public class Scene { @NotNull @Column(name = "user_id", nullable = false) + @GsonExclude private Long userId; /** The user given name of this room (e.g. 'Master bedroom') */ diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/State.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/State.java index e8e1555..01398b8 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/State.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/State.java @@ -10,6 +10,7 @@ import javax.validation.constraints.NotNull; * other properties) form a Scene */ @Entity +@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"device_id", "scene_id"})}) @Inheritance(strategy = InheritanceType.SINGLE_TABLE) public abstract class State { diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/StateRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/StateRepository.java index eda0824..d2d1278 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/StateRepository.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/StateRepository.java @@ -11,4 +11,6 @@ public interface StateRepository> extends CrudRepository findBySceneId(@Param("sceneId") long sceneId); + + Integer countByDeviceIdAndSceneId(long deviceId, long sceneId); } 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 index e68e75c..7fbc64a 100644 --- 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 @@ -44,7 +44,7 @@ public abstract class Switchable extends OutputDevice { } @Override - public State cloneState() { + public State cloneState() { final SwitchableState newState = new SwitchableState<>(); newState.setDeviceId(getId()); newState.setDevice(this); diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableState.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableState.java index 8e6a5fa..67b3118 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableState.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableState.java @@ -2,14 +2,12 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; import javax.persistence.Column; import javax.persistence.Entity; -import javax.validation.constraints.NotNull; /** Represents a state for a Switchable device */ @Entity public class SwitchableState extends State { - @Column(name = "switchable_on", nullable = false) - @NotNull + @Column(name = "switchable_on") private boolean on; public boolean isOn() {