diff --git a/starter/catpoint-parent/Image/pom.xml b/starter/catpoint-parent/Image/pom.xml new file mode 100644 index 0000000..dd1d075 --- /dev/null +++ b/starter/catpoint-parent/Image/pom.xml @@ -0,0 +1,66 @@ + + 4.0.0 + + com.udacity.catpoint + catpoint-parent + 1.0-SNAPSHOT + + + Image + 1.0-SNAPSHOT + jar + + Image + http://maven.apache.org + + + UTF-8 + 22 + 22 + + + + + + + + + + + org.slf4j + slf4j-api + 2.0.0-alpha7 + + + software.amazon.awssdk + auth + 2.22.6 + + + + software.amazon.awssdk + rekognition + 2.21.23 + + + + + org.slf4j + slf4j-api + 2.0.16 + + + + + + + + + software.amazon.awssdk + regions + 2.28.16 + + + + \ No newline at end of file diff --git a/starter/catpoint-parent/src/main/java/com/udacity/catpoint/service/AwsImageService.java b/starter/catpoint-parent/Image/src/main/java/com/udacity/catpoint/image/AwsImageService.java similarity index 99% rename from starter/catpoint-parent/src/main/java/com/udacity/catpoint/service/AwsImageService.java rename to starter/catpoint-parent/Image/src/main/java/com/udacity/catpoint/image/AwsImageService.java index 1288e27..2d9ff6d 100644 --- a/starter/catpoint-parent/src/main/java/com/udacity/catpoint/service/AwsImageService.java +++ b/starter/catpoint-parent/Image/src/main/java/com/udacity/catpoint/image/AwsImageService.java @@ -1,4 +1,4 @@ -package com.udacity.catpoint.service; +package com.udacity.catpoint.image; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/starter/catpoint-parent/src/main/java/com/udacity/catpoint/service/FakeImageService.java b/starter/catpoint-parent/Image/src/main/java/com/udacity/catpoint/image/FakeImageService.java similarity index 89% rename from starter/catpoint-parent/src/main/java/com/udacity/catpoint/service/FakeImageService.java rename to starter/catpoint-parent/Image/src/main/java/com/udacity/catpoint/image/FakeImageService.java index ac1e61f..3a2cbd5 100644 --- a/starter/catpoint-parent/src/main/java/com/udacity/catpoint/service/FakeImageService.java +++ b/starter/catpoint-parent/Image/src/main/java/com/udacity/catpoint/image/FakeImageService.java @@ -1,4 +1,4 @@ -package com.udacity.catpoint.service; +package com.udacity.catpoint.image; import java.awt.image.BufferedImage; import java.util.Random; diff --git a/starter/catpoint-parent/Image/src/main/java/module-info.java b/starter/catpoint-parent/Image/src/main/java/module-info.java new file mode 100644 index 0000000..4b95fc6 --- /dev/null +++ b/starter/catpoint-parent/Image/src/main/java/module-info.java @@ -0,0 +1,10 @@ +module com.udacity.catpoint.image { +// requires org.slf4j; + requires software.amazon.awssdk.core; + requires software.amazon.awssdk.auth; + requires software.amazon.awssdk.services.rekognition; + requires java.desktop; + requires software.amazon.awssdk.regions; + requires org.slf4j; + exports com.udacity.catpoint.image; +} \ No newline at end of file diff --git a/starter/catpoint-parent/Security/pom.xml b/starter/catpoint-parent/Security/pom.xml new file mode 100644 index 0000000..6ee1140 --- /dev/null +++ b/starter/catpoint-parent/Security/pom.xml @@ -0,0 +1,214 @@ + + 4.0.0 + + com.udacity.catpoint + catpoint-parent + 1.0-SNAPSHOT + + + Security + 1.0-SNAPSHOT + jar + + Security + http://maven.apache.org + + + UTF-8 + 22 + 22 + + + + + com.udacity.catpoint + Image + 1.0-SNAPSHOT + compiler + + + org.mockito + mockito-junit-jupiter + 5.14.1 + test + + + + + + + + + + org.springframework + spring-test + 6.1.13 + test + + + + + com.miglayout + miglayout-swing + 5.0 + + + com.google.guava + guava + 13.0-rc1 + + + + com.google.code.gson + gson + 2.11.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + make-assembly + package + + single + + + + jar-with-dependencies + + + + + com.udacity.catpoint.application.CatpointApp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/CatpointApp.java b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/CatpointApp.java new file mode 100644 index 0000000..7c420cf --- /dev/null +++ b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/CatpointApp.java @@ -0,0 +1,11 @@ +package com.udacity.catpoint.security.application; + +/** + * This is the main class that launches the application. + */ +public class CatpointApp { + public static void main(String[] args) { + CatpointGui gui = new CatpointGui(); + gui.setVisible(true); + } +} diff --git a/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/CatpointGui.java b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/CatpointGui.java new file mode 100644 index 0000000..6950567 --- /dev/null +++ b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/CatpointGui.java @@ -0,0 +1,44 @@ +package com.udacity.catpoint.security.application; + + + +import com.udacity.catpoint.image.FakeImageService; +import com.udacity.catpoint.security.data.PretendDatabaseSecurityRepositoryImpl; +import com.udacity.catpoint.security.data.SecurityRepository; +import com.udacity.catpoint.security.service.SecurityService; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; + +/** + * This is the primary JFrame for the application that contains all the top-level JPanels. + * + * We're not using any dependency injection framework, so this class also handles constructing + * all our dependencies and providing them to other classes as necessary. + */ +public class CatpointGui extends JFrame { + private SecurityRepository securityRepository = new PretendDatabaseSecurityRepositoryImpl(); + private FakeImageService imageService = new FakeImageService(); + private SecurityService securityService = new SecurityService(securityRepository, imageService); + private DisplayPanel displayPanel = new DisplayPanel(securityService); + private ControlPanel controlPanel = new ControlPanel(securityService); + private SensorPanel sensorPanel = new SensorPanel(securityService); + private ImagePanel imagePanel = new ImagePanel(securityService); + + public CatpointGui() { + setLocation(100, 100); + setSize(600, 850); + setTitle("Very Secure App"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + JPanel mainPanel = new JPanel(); + mainPanel.setLayout(new MigLayout()); + mainPanel.add(displayPanel, "wrap"); + mainPanel.add(imagePanel, "wrap"); + mainPanel.add(controlPanel, "wrap"); + mainPanel.add(sensorPanel); + + getContentPane().add(mainPanel); + + } +} diff --git a/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/ControlPanel.java b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/ControlPanel.java new file mode 100644 index 0000000..52f57dd --- /dev/null +++ b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/ControlPanel.java @@ -0,0 +1,53 @@ +package com.udacity.catpoint.security.application; + + +import com.udacity.catpoint.security.data.ArmingStatus; +import com.udacity.catpoint.security.service.SecurityService; +import com.udacity.catpoint.security.service.StyleService; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * JPanel containing the buttons to manipulate arming status of the system. + */ +public class ControlPanel extends JPanel { + + private SecurityService securityService; + private Map buttonMap; + + + public ControlPanel(SecurityService securityService) { + super(); + setLayout(new MigLayout()); + this.securityService = securityService; + + JLabel panelLabel = new JLabel("System Control"); + panelLabel.setFont(StyleService.HEADING_FONT); + + add(panelLabel, "span 3, wrap"); + + //create a map of each status type to a corresponding JButton + buttonMap = Arrays.stream(ArmingStatus.values()) + .collect(Collectors.toMap(status -> status, status -> new JButton(status.getDescription()))); + + //add an action listener to each button that applies its arming status and recolors all the buttons + buttonMap.forEach((k, v) -> { + v.addActionListener(e -> { + securityService.setArmingStatus(k); + buttonMap.forEach((status, button) -> button.setBackground(status == k ? status.getColor() : null)); + }); + }); + + //map order above is arbitrary, so loop again in order to add buttons in enum-order + Arrays.stream(ArmingStatus.values()).forEach(status -> add(buttonMap.get(status))); + + ArmingStatus currentStatus = securityService.getArmingStatus(); + buttonMap.get(currentStatus).setBackground(currentStatus.getColor()); + + + } +} diff --git a/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/DisplayPanel.java b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/DisplayPanel.java new file mode 100644 index 0000000..08f41c4 --- /dev/null +++ b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/DisplayPanel.java @@ -0,0 +1,54 @@ +package com.udacity.catpoint.security.application; + +import com.udacity.catpoint.security.data.AlarmStatus; +import com.udacity.catpoint.security.service.SecurityService; +import com.udacity.catpoint.security.service.StyleService; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; + +/** + * Displays the current status of the system. Implements the StatusListener + * interface so that it can be notified whenever the status changes. + */ +public class DisplayPanel extends JPanel implements StatusListener { + + private JLabel currentStatusLabel; + + public DisplayPanel(SecurityService securityService) { + super(); + setLayout(new MigLayout()); + + securityService.addStatusListener(this); + + JLabel panelLabel = new JLabel("Very Secure Home Security"); + JLabel systemStatusLabel = new JLabel("System Status:"); + currentStatusLabel = new JLabel(); + + panelLabel.setFont(StyleService.HEADING_FONT); + + notify(securityService.getAlarmStatus()); + + add(panelLabel, "span 2, wrap"); + add(systemStatusLabel); + add(currentStatusLabel, "wrap"); + + } + + @Override + public void notify(AlarmStatus status) { + currentStatusLabel.setText(status.getDescription()); + currentStatusLabel.setBackground(status.getColor()); + currentStatusLabel.setOpaque(true); + } + + @Override + public void catDetected(boolean catDetected) { + // no behavior necessary + } + + @Override + public void sensorStatusChanged() { + // no behavior necessary + } +} diff --git a/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/ImagePanel.java b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/ImagePanel.java new file mode 100644 index 0000000..13a117c --- /dev/null +++ b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/ImagePanel.java @@ -0,0 +1,92 @@ +package com.udacity.catpoint.security.application; + +import com.udacity.catpoint.security.data.AlarmStatus; +import com.udacity.catpoint.security.service.SecurityService; +import com.udacity.catpoint.security.service.StyleService; +import net.miginfocom.swing.MigLayout; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +/** Panel containing the 'camera' output. Allows users to 'refresh' the camera + * by uploading their own picture, and 'scan' the picture, sending it for image analysis + */ +public class ImagePanel extends JPanel implements StatusListener { + private SecurityService securityService; + + private JLabel cameraHeader; + private JLabel cameraLabel; + private BufferedImage currentCameraImage; + + private int IMAGE_WIDTH = 300; + private int IMAGE_HEIGHT = 225; + + public ImagePanel(SecurityService securityService) { + super(); + setLayout(new MigLayout()); + this.securityService = securityService; + securityService.addStatusListener(this); + + cameraHeader = new JLabel("Camera Feed"); + cameraHeader.setFont(StyleService.HEADING_FONT); + + cameraLabel = new JLabel(); + cameraLabel.setBackground(Color.WHITE); + cameraLabel.setPreferredSize(new Dimension(IMAGE_WIDTH, IMAGE_HEIGHT)); + cameraLabel.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY)); + + //button allowing users to select a file to be the current camera image + JButton addPictureButton = new JButton("Refresh Camera"); + addPictureButton.addActionListener(e -> { + JFileChooser chooser = new JFileChooser(); + chooser.setCurrentDirectory(new File(".")); + chooser.setDialogTitle("Select Picture"); + chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + if(chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) { + return; + } + try { + currentCameraImage = ImageIO.read(chooser.getSelectedFile()); + Image tmp = new ImageIcon(currentCameraImage).getImage(); + cameraLabel.setIcon(new ImageIcon(tmp.getScaledInstance(IMAGE_WIDTH, IMAGE_HEIGHT, Image.SCALE_SMOOTH))); + } catch (IOException |NullPointerException ioe) { + JOptionPane.showMessageDialog(null, "Invalid image selected."); + } + repaint(); + }); + + //button that sends the image to the image service + JButton scanPictureButton = new JButton("Scan Picture"); + scanPictureButton.addActionListener(e -> { + securityService.processImage(currentCameraImage); + }); + + add(cameraHeader, "span 3, wrap"); + add(cameraLabel, "span 3, wrap"); + add(addPictureButton); + add(scanPictureButton); + } + + @Override + public void notify(AlarmStatus status) { + //no behavior necessary + } + + @Override + public void catDetected(boolean catDetected) { + if(catDetected) { + cameraHeader.setText("DANGER - CAT DETECTED"); + } else { + cameraHeader.setText("Camera Feed - No Cats Detected"); + } + } + + @Override + public void sensorStatusChanged() { + //no behavior necessary + } +} diff --git a/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/SensorPanel.java b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/SensorPanel.java new file mode 100644 index 0000000..be31f74 --- /dev/null +++ b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/SensorPanel.java @@ -0,0 +1,121 @@ +package com.udacity.catpoint.security.application; + + +import com.udacity.catpoint.security.data.Sensor; +import com.udacity.catpoint.security.data.SensorType; +import com.udacity.catpoint.security.service.SecurityService; +import com.udacity.catpoint.security.service.StyleService; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; + +/** + * Panel that allows users to add sensors to their system. Sensors may be + * manually set to "active" and "inactive" to test the system. + */ +public class SensorPanel extends JPanel { + + private SecurityService securityService; + + private JLabel panelLabel = new JLabel("Sensor Management"); + private JLabel newSensorName = new JLabel("Name:"); + private JLabel newSensorType = new JLabel("Sensor Type:"); + private JTextField newSensorNameField = new JTextField(); + private JComboBox newSensorTypeDropdown = new JComboBox(SensorType.values()); + private JButton addNewSensorButton = new JButton("Add New Sensor"); + + private JPanel sensorListPanel; + private JPanel newSensorPanel; + + public SensorPanel(SecurityService securityService) { + super(); + setLayout(new MigLayout()); + this.securityService = securityService; + + panelLabel.setFont(StyleService.HEADING_FONT); + addNewSensorButton.addActionListener(e -> + addSensor(new Sensor(newSensorNameField.getText(), + SensorType.valueOf(newSensorTypeDropdown.getSelectedItem().toString())))); + + newSensorPanel = buildAddSensorPanel(); + sensorListPanel = new JPanel(); + sensorListPanel.setLayout(new MigLayout()); + + updateSensorList(sensorListPanel); + + add(panelLabel, "wrap"); + add(newSensorPanel, "span"); + add(sensorListPanel, "span"); + } + + /** + * Builds the panel with the form for adding a new sensor + */ + private JPanel buildAddSensorPanel() { + JPanel p = new JPanel(); + p.setLayout(new MigLayout()); + p.add(newSensorName); + p.add(newSensorNameField, "width 50:100:200"); + p.add(newSensorType); + p.add(newSensorTypeDropdown, "wrap"); + p.add(addNewSensorButton, "span 3"); + return p; + } + + /** + * Requests the current list of sensors and updates the provided panel to display them. Sensors + * will display in the order that they are created. + * @param p The Panel to populate with the current list of sensors + */ + private void updateSensorList(JPanel p) { + p.removeAll(); + securityService.getSensors().stream().sorted().forEach(s -> { + JLabel sensorLabel = new JLabel(String.format("%s(%s): %s", s.getName(), s.getSensorType().toString(),(s.getActive() ? "Active" : "Inactive"))); + JButton sensorToggleButton = new JButton((s.getActive() ? "Deactivate" : "Activate")); + JButton sensorRemoveButton = new JButton("Remove Sensor"); + + sensorToggleButton.addActionListener(e -> setSensorActivity(s, !s.getActive()) ); + sensorRemoveButton.addActionListener(e -> removeSensor(s)); + + //hard code some sizes, tsk tsk + p.add(sensorLabel, "width 300:300:300"); + p.add(sensorToggleButton, "width 100:100:100"); + p.add(sensorRemoveButton, "wrap"); + }); + + repaint(); + revalidate(); + } + + /** + * Asks the securityService to change a sensor activation status and then rebuilds the current sensor list + * @param sensor The sensor to update + * @param isActive The sensor's activation status + */ + private void setSensorActivity(Sensor sensor, Boolean isActive) { + securityService.changeSensorActivationStatus(sensor, isActive); + updateSensorList(sensorListPanel); + } + + /** + * Adds a sensor to the securityService and then rebuilds the sensor list + * @param sensor The sensor to add + */ + private void addSensor(Sensor sensor) { + if(securityService.getSensors().size() < 4) { + securityService.addSensor(sensor); + updateSensorList(sensorListPanel); + } else { + JOptionPane.showMessageDialog(null, "To add more than 4 sensors, please subscribe to our Premium Membership!"); + } + } + + /** + * Remove a sensor from the securityService and then rebuild the sensor list + * @param sensor The sensor to remove + */ + private void removeSensor(Sensor sensor) { + securityService.removeSensor(sensor); + updateSensorList(sensorListPanel); + } +} diff --git a/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/StatusListener.java b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/StatusListener.java new file mode 100644 index 0000000..6811191 --- /dev/null +++ b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/application/StatusListener.java @@ -0,0 +1,12 @@ +package com.udacity.catpoint.security.application; + +import com.udacity.catpoint.security.data.AlarmStatus; + +/** + * Identifies a component that should be notified whenever the system status changes + */ +public interface StatusListener { + void notify(AlarmStatus status); + void catDetected(boolean catDetected); + void sensorStatusChanged(); +} diff --git a/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/AlarmStatus.java b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/AlarmStatus.java new file mode 100644 index 0000000..6d07d69 --- /dev/null +++ b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/AlarmStatus.java @@ -0,0 +1,29 @@ +package com.udacity.catpoint.security.data; + +import java.awt.*; + +/** + * List of potential states the alarm can have. Also contains metadata about what + * text and color is associated with the alarm. + */ +public enum AlarmStatus { + NO_ALARM("Cool and Good", new Color(120,200,30)), + PENDING_ALARM("I'm in Danger...", new Color(200,150,20)), + ALARM("Awooga!", new Color(250,80,50)); + + private final String description; + private final Color color; + + AlarmStatus(String description, Color color) { + this.description = description; + this.color = color; + } + + public String getDescription() { + return description; + } + + public Color getColor() { + return color; + } +} diff --git a/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/ArmingStatus.java b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/ArmingStatus.java new file mode 100644 index 0000000..06c0d6f --- /dev/null +++ b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/ArmingStatus.java @@ -0,0 +1,29 @@ +package com.udacity.catpoint.security.data; + +import java.awt.*; + +/** + * List of potential states the security system can use to describe how the system is armed. + * Also contains metadata about what text and color is associated with the arming status. + */ +public enum ArmingStatus { + DISARMED("Disarmed", new Color(120,200,30)), + ARMED_HOME("Armed - At Home", new Color(190,180,50)), + ARMED_AWAY("Armed - Away", new Color(170,30,150)); + + private final String description; + private final Color color; + + ArmingStatus(String description, Color color) { + this.description = description; + this.color = color; + } + + public String getDescription() { + return description; + } + + public Color getColor() { + return color; + } +} diff --git a/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/PretendDatabaseSecurityRepositoryImpl.java b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/PretendDatabaseSecurityRepositoryImpl.java new file mode 100644 index 0000000..59df7c6 --- /dev/null +++ b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/PretendDatabaseSecurityRepositoryImpl.java @@ -0,0 +1,94 @@ +package com.udacity.catpoint.security.data; + + +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import com.udacity.catpoint.security.service.SecurityService; + +import java.lang.reflect.Type; +import java.util.Set; +import java.util.TreeSet; +import java.util.prefs.Preferences; + +/** + * Fake repository implementation for demo purposes. Stores state information in local + * memory and writes it to user preferences between app loads. This implementation is + * intentionally a little hard to use in unit tests, so watch out! + */ +public class PretendDatabaseSecurityRepositoryImpl implements SecurityRepository{ + private Set sensors; + private AlarmStatus alarmStatus; + private ArmingStatus armingStatus; + + //preference keys + private static final String SENSORS = "SENSORS"; + private static final String ALARM_STATUS = "ALARM_STATUS"; + private static final String ARMING_STATUS = "ARMING_STATUS"; + + private static final Preferences prefs = Preferences.userNodeForPackage(PretendDatabaseSecurityRepositoryImpl.class); + private static final Gson gson = new Gson(); //used to serialize objects into JSON + + public PretendDatabaseSecurityRepositoryImpl() { + //load system state from prefs, or else default + alarmStatus = AlarmStatus.valueOf(prefs.get(ALARM_STATUS, AlarmStatus.NO_ALARM.toString())); + armingStatus = ArmingStatus.valueOf(prefs.get(ARMING_STATUS, ArmingStatus.DISARMED.toString())); + + //we've serialized our sensor objects for storage, which should be a good warning sign that + // this is likely an impractical solution for a real system + String sensorString = prefs.get(SENSORS, null); + if(sensorString == null) { + sensors = new TreeSet<>(); + } +// else { +// Type type = new TypeToken>() { +// }.getType(); +// sensors = gson.fromJson(sensorString, type); +// } + } + + @Override + public void addSensor(Sensor sensor) { + sensors.add(sensor); + prefs.put(SENSORS, gson.toJson(sensors)); + } + + @Override + public void removeSensor(Sensor sensor) { + sensors.remove(sensor); + prefs.put(SENSORS, gson.toJson(sensors)); + } + + @Override + public void updateSensor(Sensor sensor) { + sensors.remove(sensor); + sensors.add(sensor); + prefs.put(SENSORS, gson.toJson(sensors)); + } + + @Override + public void setAlarmStatus(AlarmStatus alarmStatus) { + this.alarmStatus = alarmStatus; + prefs.put(ALARM_STATUS, this.alarmStatus.toString()); + } + + @Override + public void setArmingStatus(ArmingStatus armingStatus) { + this.armingStatus = armingStatus; + prefs.put(ARMING_STATUS, this.armingStatus.toString()); + } + + @Override + public Set getSensors() { + return sensors; + } + + @Override + public AlarmStatus getAlarmStatus() { + return alarmStatus; + } + + @Override + public ArmingStatus getArmingStatus() { + return armingStatus; + } +} diff --git a/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/SecurityRepository.java b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/SecurityRepository.java new file mode 100644 index 0000000..f7cf241 --- /dev/null +++ b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/SecurityRepository.java @@ -0,0 +1,19 @@ +package com.udacity.catpoint.security.data; + +import java.util.Set; + +/** + * Interface showing the methods our security repository will need to support + */ +public interface SecurityRepository { + void addSensor(Sensor sensor); + void removeSensor(Sensor sensor); + void updateSensor(Sensor sensor); + void setAlarmStatus(AlarmStatus alarmStatus); + void setArmingStatus(ArmingStatus armingStatus); + Set getSensors(); + AlarmStatus getAlarmStatus(); + ArmingStatus getArmingStatus(); + + +} diff --git a/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/Sensor.java b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/Sensor.java new file mode 100644 index 0000000..3c24372 --- /dev/null +++ b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/Sensor.java @@ -0,0 +1,80 @@ +package com.udacity.catpoint.security.data; + + +import com.google.common.collect.ComparisonChain; + +import java.util.Objects; +import java.util.UUID; + +/** + * Sensor POJO. Needs to know how to sort itself for display purposes. + */ +public class Sensor implements Comparable { + private UUID sensorId; + private String name; + private Boolean active; + private SensorType sensorType; + + public Sensor() {} + + public Sensor(String name, SensorType sensorType) { + this.name = name; + this.sensorType = sensorType; + this.sensorId = UUID.randomUUID(); + this.active = Boolean.FALSE; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Sensor sensor = (Sensor) o; + return sensorId.equals(sensor.sensorId); + } + + @Override + public int hashCode() { + return Objects.hash(sensorId); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + + public SensorType getSensorType() { + return sensorType; + } + + public void setSensorType(SensorType sensorType) { + this.sensorType = sensorType; + } + + public UUID getSensorId() { + return sensorId; + } + + public void setSensorId(UUID sensorId) { + this.sensorId = sensorId; + } + + @Override + public int compareTo(Sensor o) { + return ComparisonChain.start() + .compare(this.name, o.name) + .compare(this.sensorType.toString(), o.sensorType.toString()) + .compare(this.sensorId, o.sensorId) + .result(); + } +} diff --git a/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/SensorType.java b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/SensorType.java new file mode 100644 index 0000000..8f8cb34 --- /dev/null +++ b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/data/SensorType.java @@ -0,0 +1,8 @@ +package com.udacity.catpoint.security.data; + +/** + * List of available sensor types. Not currently used by system, other than for display. + */ +public enum SensorType { + DOOR, WINDOW, MOTION +} diff --git a/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/service/SecurityService.java b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/service/SecurityService.java new file mode 100644 index 0000000..12903b6 --- /dev/null +++ b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/service/SecurityService.java @@ -0,0 +1,172 @@ +package com.udacity.catpoint.security.service; +import com.udacity.catpoint.image.FakeImageService; +import com.udacity.catpoint.security.application.StatusListener; +import com.udacity.catpoint.security.data.AlarmStatus; +import com.udacity.catpoint.security.data.ArmingStatus; +import com.udacity.catpoint.security.data.SecurityRepository; +import com.udacity.catpoint.security.data.Sensor; +import java.awt.image.BufferedImage; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.stream.Collectors; +/** + * Service that receives information about changes to the security system. Responsible for + * forwarding updates to the repository and making any decisions about changing the system state. + *

+ * This is the class that should contain most of the business logic for our system, and it is the + * class you will be writing unit tests for. + */ +public class SecurityService { + private FakeImageService imageService; + private SecurityRepository securityRepository; + private Set statusListeners = new HashSet<>(); + private Boolean isCatDetected = false; + Set getActiveSensors() { + Set activeSensors = new HashSet<>(); + for (Sensor sensor : getSensors()) { + if (sensor.getActive()) { + activeSensors.add(sensor); + } + } + return activeSensors; + } + public SecurityService(SecurityRepository securityRepository, FakeImageService imageService) { + this.securityRepository = securityRepository; + this.imageService = imageService; + } + /** + * Sets the current arming status for the system. Changing the arming status + * may update both the alarm status. + * + * @param armingStatus + */ + public void setArmingStatus(ArmingStatus armingStatus) { + if (isCatDetected && armingStatus == ArmingStatus.ARMED_HOME) { + setAlarmStatus(AlarmStatus.ALARM); + } + if (armingStatus == ArmingStatus.DISARMED) { + setAlarmStatus(AlarmStatus.NO_ALARM); + } + if (armingStatus == ArmingStatus.ARMED_AWAY || armingStatus == ArmingStatus.ARMED_HOME) { + setFalseActivationStatusForSensors(getActiveSensors()); + } + securityRepository.setArmingStatus(armingStatus); + statusListeners.forEach(sl -> sl.sensorStatusChanged()); + } + public void setFalseActivationStatusForSensors(Set sensors) { + sensors.forEach(sensor -> { + sensor.setActive(true); + changeSensorActivationStatus(sensor, false); + }); + } + /** + * Internal method that handles alarm status changes based on whether + * the camera currently shows a cat. + * + * @param cat True if a cat is detected, otherwise false. + */ + public void catDetected(Boolean cat) { + boolean noInactiveSensors = true; + isCatDetected = cat; + if (cat && getArmingStatus() == ArmingStatus.ARMED_HOME) { + setAlarmStatus(AlarmStatus.ALARM); + } else if (!cat) { + for (Sensor sensor : getSensors()) { + if (!sensor.getActive()) { + noInactiveSensors = false; + break; + } + } + if (noInactiveSensors) { + setAlarmStatus(AlarmStatus.NO_ALARM); + } + } + statusListeners.forEach(sl -> sl.catDetected(cat)); + } + /** + * Register the StatusListener for alarm system updates from within the SecurityService. + * + * @param statusListener + */ + public void addStatusListener(StatusListener statusListener) { + statusListeners.add(statusListener); + } + public void removeStatusListener(StatusListener statusListener) { + statusListeners.remove(statusListener); + } + /** + * Change the alarm status of the system and notify all listeners. + * + * @param status + */ + public void setAlarmStatus(AlarmStatus status) { + securityRepository.setAlarmStatus(status); + statusListeners.forEach(sl -> sl.notify(status)); + } + /** + * Internal method for updating the alarm status when a sensor has been activated. + */ + private void handleSensorActivated() { + if (securityRepository.getArmingStatus() == ArmingStatus.DISARMED) { + return; //no problem if the system is disarmed + } + switch (securityRepository.getAlarmStatus()) { + case NO_ALARM -> setAlarmStatus(AlarmStatus.PENDING_ALARM); + case PENDING_ALARM -> setAlarmStatus(AlarmStatus.ALARM); + } + } + /** + * Internal method for updating the alarm status when a sensor has been deactivated + */ + public void handleSensorDeactivated() { + switch (securityRepository.getAlarmStatus()) { + case PENDING_ALARM -> setAlarmStatus(AlarmStatus.NO_ALARM); + case ALARM -> setAlarmStatus(AlarmStatus.PENDING_ALARM); + } + } + /** + * Change the activation status for the specified sensor and update alarm status if necessary. + * + * @param sensor + * @param active + */ + public void changeSensorActivationStatus(Sensor sensor, Boolean active) { + AlarmStatus actualAlarmStatus = securityRepository.getAlarmStatus(); + if(actualAlarmStatus != AlarmStatus.ALARM){ + if (!sensor.getActive() && active) { + handleSensorActivated(); + } else if (sensor.getActive() && !active) { + handleSensorDeactivated(); + } + } + sensor.setActive(active); + securityRepository.updateSensor(sensor); + } + /** + * Send an image to the SecurityService for processing. The securityService will use its provided + * ImageService to analyze the image for cats and update the alarm status accordingly. + * + * @param currentCameraImage + */ + public void processImage(BufferedImage currentCameraImage) { + catDetected(imageService.imageContainsCat(currentCameraImage, 50.0f)); + } + public AlarmStatus getAlarmStatus() { + return securityRepository.getAlarmStatus(); + } + public Set getSensors() { + return securityRepository.getSensors(); + } + public void addSensor(Sensor sensor) { + securityRepository.addSensor(sensor); + } + public void removeSensor(Sensor sensor) { + securityRepository.removeSensor(sensor); + } + public ArmingStatus getArmingStatus() { + return securityRepository.getArmingStatus(); + } +} \ No newline at end of file diff --git a/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/service/StyleService.java b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/service/StyleService.java new file mode 100644 index 0000000..6c474f4 --- /dev/null +++ b/starter/catpoint-parent/Security/src/main/java/com/udacity/catpoint/security/service/StyleService.java @@ -0,0 +1,12 @@ +package com.udacity.catpoint.security.service; + +import java.awt.*; + +/** + * Simple "service" for providing style information. + */ +public class StyleService { + + public static Font HEADING_FONT = new Font("Sans Serif", Font.BOLD, 24); + +} diff --git a/starter/catpoint-parent/Security/src/main/java/module-info.java b/starter/catpoint-parent/Security/src/main/java/module-info.java new file mode 100644 index 0000000..e325005 --- /dev/null +++ b/starter/catpoint-parent/Security/src/main/java/module-info.java @@ -0,0 +1,15 @@ +module com.udacity.catpoint.security { + requires org.slf4j; + requires software.amazon.awssdk.core; + requires software.amazon.awssdk.auth; + requires software.amazon.awssdk.services.rekognition; + requires java.desktop; + requires software.amazon.awssdk.regions; + requires com.udacity.catpoint.image; + requires miglayout.swing; + requires java.prefs; + requires com.google.gson; + requires guava; + + exports com.udacity.catpoint.security.data to com.google.common; +} \ No newline at end of file diff --git a/starter/catpoint-parent/Security/src/test/java/com/udacity/catpoint/AppTest.java b/starter/catpoint-parent/Security/src/test/java/com/udacity/catpoint/AppTest.java new file mode 100644 index 0000000..232184f --- /dev/null +++ b/starter/catpoint-parent/Security/src/test/java/com/udacity/catpoint/AppTest.java @@ -0,0 +1,265 @@ +package com.udacity.catpoint; + +import com.udacity.catpoint.image.AwsImageService; +import com.udacity.catpoint.image.FakeImageService; +import com.udacity.catpoint.security.application.StatusListener; +import com.udacity.catpoint.security.data.*; + +import com.udacity.catpoint.security.service.SecurityService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.awt.image.BufferedImage; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; +//import static org.junit.jupiter.api.Assertions.assertFalse; + +@ExtendWith(MockitoExtension.class) +public class AppTest { + private Sensor sensor; + + private final String random = UUID.randomUUID().toString(); + @Mock + private StatusListener statusListener; + + @Mock + private AwsImageService awsImageService; + @Mock + private FakeImageService fakeImageService; + @Mock + private SecurityRepository securityRepository = new PretendDatabaseSecurityRepositoryImpl(); + + private SecurityService securityService = new SecurityService(securityRepository, fakeImageService); + private Sensor getNewSensor() { + return new Sensor(random, SensorType.DOOR); + } + + Set sensors = new HashSet<>(); + Sensor sensor1 = new Sensor("Door", SensorType.DOOR); + Sensor sensor2 = new Sensor("Window", SensorType.WINDOW); + Sensor sensor3 = new Sensor("Motion", SensorType.MOTION); + + private Set getSixSampleSensors(boolean activeState) { + Set sensors = new HashSet<>(); + for (int i = 1; i < 6; i++) { + Sensor sensor = new Sensor("sampleSensor" + i, SensorType.DOOR); + sensor.setActive(activeState); + sensors.add(sensor); + } + return sensors; + } + + @BeforeEach + void setUpTest() { + securityService = new SecurityService(securityRepository, fakeImageService); + sensor = getNewSensor(); + + + sensors.add(sensor1); + sensors.add(sensor2); + sensors.add(sensor3); + } + + //#1 + @Test + void alarmArmed_sensorActivated_pendingSystem() { + given(securityRepository.getArmingStatus()).willReturn(ArmingStatus.ARMED_HOME); + given(securityRepository.getAlarmStatus()).willReturn(AlarmStatus.NO_ALARM); + securityService.changeSensorActivationStatus(sensor, true); + verify(securityRepository, times(1)).setAlarmStatus(AlarmStatus.PENDING_ALARM); + } + + //#2 + @Test + void alarmArmed_sensorActivated_alreadyPending_putStatusToAlarm() { + given(securityRepository.getArmingStatus()).willReturn(ArmingStatus.ARMED_HOME); + given(securityRepository.getAlarmStatus()).willReturn(AlarmStatus.PENDING_ALARM); + securityService.changeSensorActivationStatus(sensor, true); + verify(securityRepository, times(1)).setAlarmStatus(AlarmStatus.ALARM); + } + + //#3 + @Test + void pendingAlarm_sensorInactive_SetNoAlarmState() { + when(securityRepository.getAlarmStatus()).thenReturn(AlarmStatus.PENDING_ALARM); + System.out.println("123123123 " + sensor.getActive()); + sensor.setActive(true); + securityService.changeSensorActivationStatus(sensor, false); + verify(securityRepository, times(1)).setAlarmStatus(AlarmStatus.NO_ALARM); + } + + //#4 + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void alarmActive_sensorChanging_doNotAffectAlarmState(boolean status) { + given(securityRepository.getAlarmStatus()).willReturn(AlarmStatus.ALARM); + + securityService.changeSensorActivationStatus(sensor, status); + verify(securityRepository, never()).setAlarmStatus(AlarmStatus.NO_ALARM); + verify(securityRepository, never()).setAlarmStatus(AlarmStatus.PENDING_ALARM); + + } + + //#5 + @Test + void sensorActivated_inWhileAlreadyActive_andSystemInPendingState_setStatusToAlarm() { + when(securityRepository.getAlarmStatus()).thenReturn(AlarmStatus.NO_ALARM); + sensor.setActive(Boolean.FALSE); + securityService.changeSensorActivationStatus(sensor, false); + verify(securityRepository, never()).setAlarmStatus(any(AlarmStatus.class)); + } + + //Tests Requirement #6. If a sensor is deactivated while already inactive, make no changes to the alarm state. + @ParameterizedTest + @EnumSource(AlarmStatus.class) + @DisplayName("Test 6") + public void sensorDeactivated_whileAlreadyInactive_alarmStateDoesNotChange(AlarmStatus alarmStatus) { + + securityService.changeSensorActivationStatus(sensor, false); + when(securityRepository.getAlarmStatus()).thenReturn(alarmStatus); + securityService.changeSensorActivationStatus(sensor, false); + + verify(securityRepository, never()).setAlarmStatus(any(AlarmStatus.class)); + + } + + //Tests Requirement #7. If the image service identifies an image containing a cat while the system is armed-home, + //put the system into alarm status. + @Test + @DisplayName("Test 7") + public void imageServiceIdentifiesImageContainingCat_while_SystemArmedHome_putSystemIntoAlarmStatus() { + + when(securityRepository.getArmingStatus()).thenReturn(ArmingStatus.ARMED_HOME); + when(fakeImageService.imageContainsCat(any(), anyFloat())).thenReturn(true); + securityService.processImage(mock(BufferedImage.class)); + + verify(securityRepository, times(1)).setAlarmStatus(AlarmStatus.ALARM); + } + + + //Tests Requirement #8. If the image service identifies an image that does not contain a cat, change the status + //to no alarm as long as the sensors are not active. + @Test + @DisplayName("Test 8") + public void imageServiceIdentifiesImage_doesNotContainCat_changeStatusToNoAlarm_ifSensorsNotActive() { + + when(fakeImageService.imageContainsCat(any(), anyFloat())).thenReturn(false); + Set sensors2 = Set.of(sensor, sensor1, sensor2, sensor3); + sensors.iterator().next().setActive(false); + + securityService.processImage(mock(BufferedImage.class)); +// when(securityRepository.image()).thenReturn(sensors); +// securityService.setArmingStatus(ArmingStatus.ARMED_HOME); +// securityService.processImage(mock(BufferedImage.class)); + + verify(securityRepository, times(1)).setAlarmStatus(AlarmStatus.NO_ALARM); + } + + //9 + @Test + void inCaseSystemDisarmed_SetToNoAlarmState() { + securityService.setArmingStatus(ArmingStatus.DISARMED); + + verify(securityRepository, times(1)).setAlarmStatus(AlarmStatus.NO_ALARM); + } + + //Tests Requirement #10. If the system is armed, reset all sensors to inactive. + @ParameterizedTest + @EnumSource(value = ArmingStatus.class, names = {"ARMED_AWAY", "ARMED_HOME"}) + @DisplayName("Test 10") + public void systemArmed_setAllSensorsToInactive(ArmingStatus armingStatus) { + + when(securityRepository.getSensors()).thenReturn(sensors); + when(securityRepository.getArmingStatus()).thenReturn(ArmingStatus.DISARMED); + securityService.changeSensorActivationStatus(sensor, true); +// securityService.changeSensorActivationStatus(sensor2,true); + securityService.setArmingStatus(armingStatus); + + assert (securityRepository).getSensors().stream().noneMatch(Sensor::getActive); + + } + + //11 + @Test + void ifSystemArmedHomeWhileImageServiceIdentifiesCat_changeStatusToAlarm() { + BufferedImage catImage = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB); + given(fakeImageService.imageContainsCat(any(), anyFloat())).willReturn(true); + given(securityRepository.getArmingStatus()).willReturn(ArmingStatus.DISARMED); + securityService.processImage(catImage); + securityService.setArmingStatus(ArmingStatus.ARMED_HOME); + + verify(securityRepository, times(1)).setAlarmStatus(AlarmStatus.ALARM); + } + + + //Tests if a sensor is added in SecurityService, that it is happening in SecurityRepository + @Test + @DisplayName("Test 13") + public void addSensor_to_securityService_isAddedTo_securityRepository() { + + securityService.addSensor(sensor1); + verify(securityRepository,times(1)).addSensor(sensor1); + + } + + //Tests if a sensor is removed in SecurityService, that it is happening in SecurityRepository + @Test + @DisplayName("Test 14") + public void removeSensor_from_securityService_isRemovedFrom_securityRepository() { + + securityService.removeSensor(sensor2); + verify(securityRepository,times(1)).removeSensor(sensor2); + + } + + //Tests if getSenors is called in SecurityService, that it returns sensors from SecurityRepository + @Test + @DisplayName("Test 17") + public void getSensors_calledInSecurityService_returnsSensorsFromSecurityRepository(){ + + when(securityService.getSensors()).thenReturn(sensors); + + System.out.println(securityService.getSensors()); + System.out.println(securityRepository.getSensors()); + + } + +// @Test +// void coverage() { +// when(securityRepository.getSensors()).thenReturn(sensors); +// securityService.changeSensorActivationStatus(sensor, false); +// securityService.addSensor(sensor1); +//// securityService.getAlarmStatus(); +//// securityService.removeSensor(sensor1); +// securityService.setFalseActivationStatusForSensors(sensors); +// securityService.catDetected(false); +// +// securityService.setAlarmStatus(AlarmStatus.ALARM); +// securityService.handleSensorDeactivated(); +//// securityService.getAlarmStatus(); +// } + + // More tests covering SecurityService class + @Test + void statusListenerTestCoverage() { + securityService.addStatusListener(statusListener); + securityService.removeStatusListener(statusListener); + } + + @Test + void sensorTestCoverage() { + securityService.addSensor(sensor); + securityService.removeSensor(sensor); + } +} diff --git a/starter/catpoint-parent/pom.xml b/starter/catpoint-parent/pom.xml index b73c833..4b3e222 100644 --- a/starter/catpoint-parent/pom.xml +++ b/starter/catpoint-parent/pom.xml @@ -1,69 +1,216 @@ - 4.0.0 - - com.udacity.catpoint - catpoint-parent - 1.0-SNAPSHOT - - catpoint-parent - http://www.example.com - - - UTF-8 - 14 - 14 - - - - - - - - - - - - maven-clean-plugin - 3.1.0 - - - - maven-resources-plugin - 3.0.2 - - - maven-compiler-plugin - 3.8.0 - - - maven-surefire-plugin - 2.22.1 - - - maven-jar-plugin - 3.0.2 - - - maven-install-plugin - 2.5.2 - - - maven-deploy-plugin - 2.8.2 - - - - maven-site-plugin - 3.9.1 - - - maven-project-info-reports-plugin - 3.1.1 - - - - - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + com.udacity.catpoint + catpoint-parent + 1.0-SNAPSHOT + pom + + catpoint-parent + http://www.example.com + + Image + Security + + + + UTF-8 + 14 + 14 + + + + + + junit + junit + 4.13.2 + test + + + + org.junit.jupiter + junit-jupiter-api + 5.8.2 + test + + + + + org.junit.jupiter + junit-jupiter-params + 5.8.2 + test + + + + org.junit.jupiter + junit-jupiter-engine + 5.8.2 + test + + + + org.mockito + mockito-junit-jupiter + 4.5.1 + test + + + org.mockito + mockito-junit-jupiter + 4.5.1 + test + + + + + + + + + org.apache.logging.log4j + log4j-api + 2.14.0 + + + + org.apache.logging.log4j + log4j-core + 2.14.0 + + + + org.apache.logging.log4j + log4j-slf4j-impl + 2.7 + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.0.4 + + + + com.google.guava + guava + 13.0-rc1 + + + + com.google.code.gson + gson + 2.11.0 + + + + org.slf4j + slf4j-api + 2.0.16 + + + + software.amazon.awssdk + auth + 2.28.13 + + + + software.amazon.awssdk + rekognition + 2.28.13 + + + + + + + + + + maven-compiler-plugin + 3.8.1 + + + maven-surefire-plugin + 2.22.2 + + true + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.9.1 + + + maven-project-info-reports-plugin + 3.1.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.0.4 + + + + \ No newline at end of file