Preparations for a major refactoring
This commit is contained in:
parent
bd54762bac
commit
1526e58b06
13 changed files with 62 additions and 66 deletions
|
|
@ -5,7 +5,6 @@ import se.hal.struct.Event;
|
|||
import se.hal.struct.Sensor;
|
||||
import zutil.db.DBConnection;
|
||||
import zutil.log.LogUtil;
|
||||
import zutil.plugin.PluginData;
|
||||
import zutil.plugin.PluginManager;
|
||||
import zutil.ui.Configurator;
|
||||
import zutil.ui.Configurator.PostConfigurationActionListener;
|
||||
|
|
@ -57,27 +56,27 @@ public class ControllerManager implements HalSensorReportListener,
|
|||
/////////////////////////////// SENSORS ///////////////////////////////////
|
||||
|
||||
public void register(Sensor sensor) {
|
||||
if(sensor.getDeviceData() == null) {
|
||||
if(sensor.getDeviceConfig() == null) {
|
||||
logger.warning("Sensor data is null: "+ sensor);
|
||||
return;
|
||||
}
|
||||
if(!availableSensors.contains(sensor.getDeviceData().getClass())) {
|
||||
logger.warning("Sensor data plugin not available: "+ sensor.getDeviceData().getClass());
|
||||
if(!availableSensors.contains(sensor.getDeviceConfig().getClass())) {
|
||||
logger.warning("Sensor data plugin not available: "+ sensor.getDeviceConfig().getClass());
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Registering new sensor(id: "+ sensor.getId() +"): "+ sensor.getDeviceData().getClass());
|
||||
logger.info("Registering new sensor(id: "+ sensor.getId() +"): "+ sensor.getDeviceConfig().getClass());
|
||||
Class<? extends HalSensorController> c = sensor.getController();
|
||||
HalSensorController controller = getControllerInstance(c);
|
||||
|
||||
if(controller != null)
|
||||
controller.register(sensor.getDeviceData());
|
||||
controller.register(sensor.getDeviceConfig());
|
||||
registeredSensors.add(sensor);
|
||||
detectedSensors.remove(findSensor(sensor.getDeviceData(), detectedSensors)); // Remove if this device was detected
|
||||
detectedSensors.remove(findSensor(sensor.getDeviceConfig(), detectedSensors)); // Remove if this device was detected
|
||||
}
|
||||
|
||||
public void deregister(Sensor sensor){
|
||||
if(sensor.getDeviceData() == null) {
|
||||
if(sensor.getDeviceConfig() == null) {
|
||||
logger.warning("Sensor data is null: "+ sensor);
|
||||
return;
|
||||
}
|
||||
|
|
@ -85,8 +84,8 @@ public class ControllerManager implements HalSensorReportListener,
|
|||
Class<? extends HalSensorController> c = sensor.getController();
|
||||
HalSensorController controller = (HalSensorController) controllerMap.get(c);;
|
||||
if (controller != null) {
|
||||
logger.info("Deregistering sensor(id: "+ sensor.getId() +"): "+ sensor.getDeviceData().getClass());
|
||||
controller.deregister(sensor.getDeviceData());
|
||||
logger.info("Deregistering sensor(id: "+ sensor.getId() +"): "+ sensor.getDeviceConfig().getClass());
|
||||
controller.deregister(sensor.getDeviceConfig());
|
||||
registeredSensors.remove(sensor);
|
||||
removeControllerIfEmpty(controller);
|
||||
} else {
|
||||
|
|
@ -126,7 +125,7 @@ public class ControllerManager implements HalSensorReportListener,
|
|||
detectedSensors.add(sensor);
|
||||
}
|
||||
}
|
||||
sensor.setDeviceData(sensorData); // Set the latest data
|
||||
sensor.setDeviceConfig(sensorData); // Set the latest data
|
||||
|
||||
}catch (SQLException e){
|
||||
logger.log(Level.WARNING, "Unable to store sensor report", e);
|
||||
|
|
@ -136,7 +135,7 @@ public class ControllerManager implements HalSensorReportListener,
|
|||
private static Sensor findSensor(HalSensorData sensorData, List<Sensor> list){
|
||||
for (int i=0; i<list.size(); ++i) { // Don't use foreach for concurrency reasons
|
||||
Sensor s = list.get(i);
|
||||
if (sensorData.equals(s.getDeviceData())) {
|
||||
if (sensorData.equals(s.getDeviceConfig())) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
|
@ -146,27 +145,27 @@ public class ControllerManager implements HalSensorReportListener,
|
|||
//////////////////////////////// EVENTS ///////////////////////////////////
|
||||
|
||||
public void register(Event event) {
|
||||
if(event.getDeviceData() == null) {
|
||||
if(event.getDeviceConfig() == null) {
|
||||
logger.warning("Event data is null: "+ event);
|
||||
return;
|
||||
}
|
||||
if(!availableEvents.contains(event.getDeviceData().getClass())) {
|
||||
logger.warning("Event data plugin not available: "+ event.getDeviceData().getClass());
|
||||
if(!availableEvents.contains(event.getDeviceConfig().getClass())) {
|
||||
logger.warning("Event data plugin not available: "+ event.getDeviceConfig().getClass());
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Registering new event(id: "+ event.getId() +"): "+ event.getDeviceData().getClass());
|
||||
logger.info("Registering new event(id: "+ event.getId() +"): "+ event.getDeviceConfig().getClass());
|
||||
Class<? extends HalEventController> c = event.getController();
|
||||
HalEventController controller = getControllerInstance(c);
|
||||
|
||||
if(controller != null)
|
||||
controller.register(event.getDeviceData());
|
||||
controller.register(event.getDeviceConfig());
|
||||
registeredEvents.add(event);
|
||||
detectedEvents.remove(findEvent(event.getDeviceData(), detectedEvents)); // Remove if this device was detected
|
||||
detectedEvents.remove(findEvent(event.getDeviceConfig(), detectedEvents)); // Remove if this device was detected
|
||||
}
|
||||
|
||||
public void deregister(Event event){
|
||||
if(event.getDeviceData() == null) {
|
||||
if(event.getDeviceConfig() == null) {
|
||||
logger.warning("Event data is null: "+ event);
|
||||
return;
|
||||
}
|
||||
|
|
@ -174,8 +173,8 @@ public class ControllerManager implements HalSensorReportListener,
|
|||
Class<? extends HalEventController> c = event.getController();
|
||||
HalEventController controller = (HalEventController) controllerMap.get(c);
|
||||
if (controller != null) {
|
||||
logger.info("Deregistering event(id: "+ event.getId() +"): "+ event.getDeviceData().getClass());
|
||||
controller.deregister(event.getDeviceData());
|
||||
logger.info("Deregistering event(id: "+ event.getId() +"): "+ event.getDeviceConfig().getClass());
|
||||
controller.deregister(event.getDeviceConfig());
|
||||
registeredEvents.remove(event);
|
||||
removeControllerIfEmpty(controller);
|
||||
} else {
|
||||
|
|
@ -215,7 +214,7 @@ public class ControllerManager implements HalSensorReportListener,
|
|||
detectedEvents.add(event);
|
||||
}
|
||||
}
|
||||
event.setDeviceData(eventData); // Set the latest data
|
||||
event.setDeviceConfig(eventData); // Set the latest data
|
||||
|
||||
}catch (SQLException e){
|
||||
logger.log(Level.WARNING, "Unable to store event report", e);
|
||||
|
|
@ -225,7 +224,7 @@ public class ControllerManager implements HalSensorReportListener,
|
|||
private static Event findEvent(HalEventData eventData, List<Event> list){
|
||||
for (int i=0; i<list.size(); ++i) { // Don't use foreach for concurrency reasons
|
||||
Event e = list.get(i);
|
||||
if (eventData.equals(e.getDeviceData())) {
|
||||
if (eventData.equals(e.getDeviceConfig())) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
|
@ -235,8 +234,8 @@ public class ControllerManager implements HalSensorReportListener,
|
|||
public void send(Event event){
|
||||
HalEventController controller = getControllerInstance(event.getController());
|
||||
if(controller != null) {
|
||||
controller.send(event.getDeviceData());
|
||||
reportReceived(event.getDeviceData()); // save action to db
|
||||
controller.send(event.getDeviceConfig());
|
||||
reportReceived(event.getDeviceConfig()); // save action to db
|
||||
}
|
||||
else
|
||||
logger.warning("No controller found for event id: "+ event.getId());
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ public class PCDataSynchronizationClient implements HalDaemon {
|
|||
sensor.setType(sensorDTO.type);
|
||||
sensor.setUser(user);
|
||||
|
||||
sensor.getDeviceConfig().setValues(JSONParser.read(sensorDTO.config)).applyConfiguration();
|
||||
sensor.getDeviceConfigurator().setValues(JSONParser.read(sensorDTO.config)).applyConfiguration();
|
||||
sensor.save(db);
|
||||
} catch (Exception e){
|
||||
logger.warning("Unable to register external sensor: " +
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ public class PCDataSynchronizationDaemon extends ThreadedTCPNetworkServer implem
|
|||
dto.sensorId = sensor.getId();
|
||||
dto.name = sensor.getName();
|
||||
dto.type = sensor.getType();
|
||||
dto.config = JSONWriter.toString(sensor.getDeviceConfig().getValuesAsNode());
|
||||
dto.config = JSONWriter.toString(sensor.getDeviceConfigurator().getValuesAsNode());
|
||||
rsp.sensors.add(dto);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,11 +55,11 @@ public class SensorDataAggregatorDaemon implements HalDaemon {
|
|||
}
|
||||
|
||||
public void aggregateSensor(Sensor sensor) {
|
||||
if(sensor.getDeviceData() == null){
|
||||
if(sensor.getDeviceConfig() == null){
|
||||
logger.fine("The sensor type is not supported - ignoring it");
|
||||
return;
|
||||
}
|
||||
logger.fine("The sensor is of type: " + sensor.getDeviceData().getClass().getName());
|
||||
logger.fine("The sensor is of type: " + sensor.getDeviceConfig().getClass().getName());
|
||||
|
||||
long aggregationStartTime = System.currentTimeMillis();
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ public class SensorDataAggregatorDaemon implements HalDaemon {
|
|||
*/
|
||||
private void aggregateRawData(Sensor sensor, AggregationPeriodLength aggrPeriodLength, long ageLimitInMs, int expectedSampleCount, long aggregationStartTime){
|
||||
long sensorId = sensor.getId();
|
||||
AggregationMethod aggrMethod = sensor.getDeviceData().getAggregationMethod();
|
||||
AggregationMethod aggrMethod = sensor.getDeviceConfig().getAggregationMethod();
|
||||
DBConnection db = HalContext.getDB();
|
||||
PreparedStatement stmt = null;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import se.hal.struct.Event;
|
|||
import se.hal.struct.User;
|
||||
import zutil.db.DBConnection;
|
||||
import zutil.io.file.FileUtil;
|
||||
import zutil.net.http.HttpHeader;
|
||||
import zutil.parser.Templator;
|
||||
import zutil.ui.Configurator;
|
||||
import zutil.ui.Configurator.ConfigurationParam;
|
||||
|
|
@ -60,7 +59,7 @@ public class EventConfigHttpPage extends HalHttpPage {
|
|||
event.setName(request.get("name"));
|
||||
event.setType(request.get("type"));
|
||||
event.setUser(localUser);
|
||||
event.getDeviceConfig().setValues(request).applyConfiguration();
|
||||
event.getDeviceConfigurator().setValues(request).applyConfiguration();
|
||||
event.save(db);
|
||||
ControllerManager.getInstance().register(event);
|
||||
break;
|
||||
|
|
@ -70,7 +69,7 @@ public class EventConfigHttpPage extends HalHttpPage {
|
|||
event.setName(request.get("name"));
|
||||
event.setType(request.get("type"));
|
||||
event.setUser(localUser);
|
||||
event.getDeviceConfig().setValues(request).applyConfiguration();
|
||||
event.getDeviceConfigurator().setValues(request).applyConfiguration();
|
||||
event.save(db);
|
||||
}
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import se.hal.util.HistoryDataListSqlResult;
|
|||
import se.hal.util.HistoryDataListSqlResult.HistoryData;
|
||||
import zutil.db.DBConnection;
|
||||
import zutil.io.file.FileUtil;
|
||||
import zutil.net.http.HttpHeader;
|
||||
import zutil.parser.Templator;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
|
|
@ -41,7 +40,7 @@ public class EventOverviewHttpPage extends HalHttpPage {
|
|||
if(request.containsKey("action")){
|
||||
// change event data
|
||||
Event event = Event.getEvent(db, id);
|
||||
HalEventData eventData = event.getDeviceData();
|
||||
HalEventData eventData = event.getDeviceConfig();
|
||||
if (eventData instanceof SwitchEventData){
|
||||
if ( request.containsKey("data") && "on".equals(request.get("data")))
|
||||
((SwitchEventData)eventData).turnOn();
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import zutil.parser.Templator;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
|
|
@ -113,7 +112,7 @@ public class MapHttpPage extends HalHttpPage implements HalHttpPage.HalJsonPage
|
|||
DataNode sensorsNode = new DataNode(DataNode.DataType.List);
|
||||
for (Sensor sensor : Sensor.getLocalSensors(db)) {
|
||||
DataNode sensorNode = getDeviceNode(sensor);
|
||||
sensorNode.set("data", sensor.getDeviceData().getData());
|
||||
sensorNode.set("data", sensor.getDeviceConfig().getData());
|
||||
sensorsNode.add(sensorNode);
|
||||
}
|
||||
root.set("sensors", sensorsNode);
|
||||
|
|
@ -121,7 +120,7 @@ public class MapHttpPage extends HalHttpPage implements HalHttpPage.HalJsonPage
|
|||
DataNode eventsNode = new DataNode(DataNode.DataType.List);
|
||||
for (Event event : Event.getLocalEvents(db)) {
|
||||
DataNode eventNode = getDeviceNode(event);
|
||||
eventNode.set("data", event.getDeviceData().getData());
|
||||
eventNode.set("data", event.getDeviceConfig().getData());
|
||||
eventsNode.add(eventNode);
|
||||
}
|
||||
root.set("events", eventsNode);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import se.hal.util.AggregateDataListSqlResult.AggregateData;
|
|||
import se.hal.util.UTCTimeUtility;
|
||||
import zutil.db.DBConnection;
|
||||
import zutil.io.file.FileUtil;
|
||||
import zutil.net.http.HttpHeader;
|
||||
import zutil.parser.DataNode;
|
||||
import zutil.parser.Templator;
|
||||
|
||||
|
|
@ -114,8 +113,8 @@ public class PCOverviewHttpPage extends HalHttpPage implements HalHttpPage.HalJs
|
|||
private List<Sensor> getSensorList(DBConnection db) throws SQLException {
|
||||
List<Sensor> sensors = new ArrayList<>();
|
||||
for (Sensor sensor : Sensor.getSensors(db)) {
|
||||
if (sensor.getDeviceData() != null &&
|
||||
sensor.getDeviceData() instanceof PowerConsumptionSensorData)
|
||||
if (sensor.getDeviceConfig() != null &&
|
||||
sensor.getDeviceConfig() instanceof PowerConsumptionSensorData)
|
||||
sensors.add(sensor);
|
||||
}
|
||||
return sensors;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import se.hal.struct.Sensor;
|
|||
import se.hal.struct.User;
|
||||
import zutil.db.DBConnection;
|
||||
import zutil.io.file.FileUtil;
|
||||
import zutil.net.http.HttpHeader;
|
||||
import zutil.parser.Templator;
|
||||
import zutil.ui.Configurator;
|
||||
import zutil.ui.Configurator.ConfigurationParam;
|
||||
|
|
@ -62,7 +61,7 @@ public class SensorConfigHttpPage extends HalHttpPage {
|
|||
sensor.setType(request.get("type"));
|
||||
sensor.setSynced(Boolean.parseBoolean(request.get("sync")));
|
||||
sensor.setUser(localUser);
|
||||
sensor.getDeviceConfig().setValues(request).applyConfiguration();
|
||||
sensor.getDeviceConfigurator().setValues(request).applyConfiguration();
|
||||
sensor.save(db);
|
||||
ControllerManager.getInstance().register(sensor);
|
||||
break;
|
||||
|
|
@ -72,7 +71,7 @@ public class SensorConfigHttpPage extends HalHttpPage {
|
|||
sensor.setName(request.get("name"));
|
||||
sensor.setType(request.get("type"));
|
||||
sensor.setSynced(Boolean.parseBoolean(request.get("sync")));
|
||||
sensor.getDeviceConfig().setValues(request).applyConfiguration();
|
||||
sensor.getDeviceConfigurator().setValues(request).applyConfiguration();
|
||||
sensor.save(db);
|
||||
}
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import java.util.logging.Level;
|
|||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Created by ezivkoc on 2016-01-15.
|
||||
* Created by Ziver on 2016-01-15.
|
||||
*/
|
||||
public abstract class AbstractDevice<T> extends DBBean {
|
||||
private static final Logger logger = LogUtil.getLogger();
|
||||
|
|
@ -21,10 +21,12 @@ public abstract class AbstractDevice<T> extends DBBean {
|
|||
// Sensor specific data
|
||||
private String name;
|
||||
private String type;
|
||||
private String config; // only used to store the deviceData configuration in DB
|
||||
private String config; // only used to store the deviceConfig configuration in DB
|
||||
|
||||
// Sensor specific data
|
||||
private transient T deviceData;
|
||||
/** Sensor specific configuration **/
|
||||
private transient T deviceConfig;
|
||||
/** latest device data received **/
|
||||
private transient Object latestDeviceData;
|
||||
|
||||
// User configuration
|
||||
@DBColumn("user_id")
|
||||
|
|
@ -38,8 +40,8 @@ public abstract class AbstractDevice<T> extends DBBean {
|
|||
|
||||
|
||||
|
||||
public Configurator<T> getDeviceConfig() {
|
||||
T obj = getDeviceData();
|
||||
public Configurator<T> getDeviceConfigurator() {
|
||||
T obj = getDeviceConfig();
|
||||
if (obj != null) {
|
||||
Configurator<T> configurator = new Configurator<>(obj);
|
||||
configurator.setPreConfigurationListener(ControllerManager.getInstance());
|
||||
|
|
@ -48,18 +50,18 @@ public abstract class AbstractDevice<T> extends DBBean {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
public T getDeviceData() {
|
||||
if (deviceData == null || !deviceData.getClass().getName().equals(type)) {
|
||||
public T getDeviceConfig() {
|
||||
if (deviceConfig == null || !deviceConfig.getClass().getName().equals(type)) {
|
||||
try {
|
||||
Class c = Class.forName(type);
|
||||
deviceData = (T) c.newInstance();
|
||||
deviceConfig = (T) c.newInstance();
|
||||
|
||||
applyConfig();
|
||||
} catch (Exception e) {
|
||||
logger.log(Level.SEVERE, "Unable instantiate DeviceData: "+type, e);
|
||||
}
|
||||
}
|
||||
return deviceData;
|
||||
return deviceConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -67,20 +69,20 @@ public abstract class AbstractDevice<T> extends DBBean {
|
|||
* And the current config will be applied on the new DeviceData.
|
||||
* DeviceData will be reset if the input is set as null.
|
||||
*/
|
||||
public void setDeviceData(T data) {
|
||||
public void setDeviceConfig(T data) {
|
||||
if(data != null) {
|
||||
deviceData = data;
|
||||
deviceConfig = data;
|
||||
type = data.getClass().getName();
|
||||
applyConfig(); // TODO: this is a bit clunky, should probably be solved in another way
|
||||
} else {
|
||||
deviceData = null;
|
||||
deviceConfig = null;
|
||||
type = null;
|
||||
config = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void save(DBConnection db) throws SQLException {
|
||||
if (deviceData != null)
|
||||
if (deviceConfig != null)
|
||||
updateConfigString();
|
||||
else
|
||||
this.config = null;
|
||||
|
|
@ -91,7 +93,7 @@ public abstract class AbstractDevice<T> extends DBBean {
|
|||
* Will update the config String that will be stored in DB.
|
||||
*/
|
||||
protected void updateConfigString() {
|
||||
Configurator<T> configurator = getDeviceConfig();
|
||||
Configurator<T> configurator = getDeviceConfigurator();
|
||||
this.config = JSONWriter.toString(configurator.getValuesAsNode());
|
||||
}
|
||||
/**
|
||||
|
|
@ -100,7 +102,7 @@ public abstract class AbstractDevice<T> extends DBBean {
|
|||
*/
|
||||
protected void applyConfig(){
|
||||
if (config != null && !config.isEmpty()) {
|
||||
Configurator<T> configurator = getDeviceConfig();
|
||||
Configurator<T> configurator = getDeviceConfigurator();
|
||||
configurator.setValues(JSONParser.read(config));
|
||||
configurator.applyConfiguration();
|
||||
}
|
||||
|
|
@ -131,7 +133,7 @@ public abstract class AbstractDevice<T> extends DBBean {
|
|||
if (this.type == null || !this.type.equals(type)) {
|
||||
this.type = type;
|
||||
this.config = null;
|
||||
this.deviceData = null; // invalidate current sensor data object
|
||||
this.deviceConfig = null; // invalidate current sensor data object
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,6 @@ public class Event extends AbstractDevice<HalEventData>{
|
|||
|
||||
|
||||
public Class<? extends HalEventController> getController(){
|
||||
return getDeviceData().getEventController();
|
||||
return getDeviceConfig().getEventController();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,6 @@ public class Sensor extends AbstractDevice<HalSensorData>{
|
|||
|
||||
|
||||
public Class<? extends HalSensorController> getController(){
|
||||
return getDeviceData().getSensorController();
|
||||
return getDeviceConfig().getSensorController();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,13 +83,13 @@ public class AggregateDataListSqlResult implements SQLResultHandler<ArrayList<Ag
|
|||
float estimatedData = data/confidence; //estimate the "real" value of the data by looking at the confidence value
|
||||
|
||||
// Add null data point to list if one or more periods of data is missing before this
|
||||
if (previousTimestampEnd != -1 && sensor.getDeviceData() != null){
|
||||
boolean shortInterval = timestampEnd-timestampStart < sensor.getDeviceData().getDataInterval();
|
||||
if (previousTimestampEnd != -1 && sensor.getDeviceConfig() != null){
|
||||
boolean shortInterval = timestampEnd-timestampStart < sensor.getDeviceConfig().getDataInterval();
|
||||
long distance = timestampStart - (previousTimestampEnd + 1);
|
||||
if (// Only add nulls if the report interval is smaller than the aggregated interval
|
||||
!shortInterval && distance > 0 ||
|
||||
// Only add nulls if space between aggr is larger than sensor report interval
|
||||
shortInterval && distance > sensor.getDeviceData().getDataInterval())
|
||||
shortInterval && distance > sensor.getDeviceConfig().getDataInterval())
|
||||
list.add(new AggregateData(id, previousTimestampEnd + 1, null /*Float.NaN*/, username));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue