From 93df3f4ce4e9b4e04d2a69e319fb3cc641e868d9 Mon Sep 17 00:00:00 2001 From: Matt Magoffin Date: Mon, 28 Oct 2024 16:48:49 +1300 Subject: [PATCH 01/11] NET-414: cloud integrations settings API. Squashed commit of the following: commit 06d81efd4425bfe8f16e9ed75e342699a1564b89 Author: Matt Magoffin Date: Mon Oct 28 16:48:08 2024 +1300 NET-414: work on cloud datum stream settings API. commit e8f2ddf424e1627965810eb988c5259f28d51277 Author: Matt Magoffin Date: Mon Oct 28 16:20:33 2024 +1300 NET-414: work on cloud user settings API. commit 4ae1b30228ddad52794f8617d4e9b7f1732dfe56 Author: Matt Magoffin Date: Mon Oct 28 14:40:48 2024 +1300 NET-414: integrate datum stream settings into poll task service. commit 4cce9382070e5571e436a170bf1df40b655d1cbb Author: Matt Magoffin Date: Mon Oct 28 14:22:12 2024 +1300 Remove unused variable. commit d8da11ebc072b607c3afc1c9c8ca0c20258676be Author: Matt Magoffin Date: Mon Oct 28 11:14:57 2024 +1300 NET-414: start work on datum stream publish settings support. --- .../NET-414-cloud-integrations-flux.sql | 44 +++ .../impl/DaoCloudDatumStreamPollService.java | 58 ++- .../config/CloudIntegrationsDaoConfig.java | 36 +- .../central/c2c/dao/BasicFilter.java | 4 +- .../CloudDatumStreamSettingsEntityDao.java | 57 +++ .../dao/CloudDatumStreamSettingsFilter.java | 34 ++ .../c2c/dao/UserSettingsEntityDao.java | 36 ++ ...oudDatumStreamSettingsEntityRowMapper.java | 90 +++++ ...JdbcCloudDatumStreamSettingsEntityDao.java | 151 ++++++++ .../dao/jdbc/JdbcUserSettingsEntityDao.java | 91 +++++ .../dao/jdbc/UserSettingsEntityRowMapper.java | 86 +++++ .../jdbc/sql/DeleteUserSettingsEntity.java | 71 ++++ .../SelectCloudDatumStreamSettingsEntity.java | 245 +++++++++++++ .../jdbc/sql/SelectUserSettingsEntity.java | 74 ++++ .../UpsertCloudDatumStreamSettingsEntity.java | 100 ++++++ .../jdbc/sql/UpsertUserSettingsEntity.java | 93 +++++ .../domain/BasicCloudDatumStreamSettings.java | 48 +++ .../c2c/domain/CloudDatumStreamSettings.java | 49 +++ .../CloudDatumStreamSettingsEntity.java | 169 +++++++++ .../c2c/domain/UserSettingsEntity.java | 196 ++++++++++ .../DaoCloudDatumStreamPollServiceTests.java | 18 +- .../c2c/dao/jdbc/test/CinJdbcTestUtils.java | 82 ++++- ...loudDatumStreamSettingsEntityDaoTests.java | 340 ++++++++++++++++++ .../test/JdbcUserSettingsEntityDaoTests.java | 145 ++++++++ .../central/test/CommonTestUtils.java | 12 +- ...loudIntegrationsDatumStreamPollConfig.java | 8 +- .../v1/UserCloudIntegrationsController.java | 70 +++- .../UserCloudIntegrationsSecurityAspect.java | 45 +++ .../c2c/biz/UserCloudIntegrationsBiz.java | 43 ++- .../biz/impl/DaoUserCloudIntegrationsBiz.java | 82 ++++- .../UserCloudIntegrationsBizConfig.java | 16 +- .../CloudDatumStreamSettingsEntityInput.java | 88 +++++ .../c2c/domain/UserSettingsEntityInput.java | 94 +++++ .../DaoUserCloudIntegrationsBizTests.java | 109 +++++- 34 files changed, 2853 insertions(+), 31 deletions(-) create mode 100644 solarnet-db-setup/postgres/updates/NET-414-cloud-integrations-flux.sql create mode 100644 solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/CloudDatumStreamSettingsEntityDao.java create mode 100644 solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/CloudDatumStreamSettingsFilter.java create mode 100644 solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/UserSettingsEntityDao.java create mode 100644 solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/CloudDatumStreamSettingsEntityRowMapper.java create mode 100644 solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/JdbcCloudDatumStreamSettingsEntityDao.java create mode 100644 solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/JdbcUserSettingsEntityDao.java create mode 100644 solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/UserSettingsEntityRowMapper.java create mode 100644 solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/DeleteUserSettingsEntity.java create mode 100644 solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/SelectCloudDatumStreamSettingsEntity.java create mode 100644 solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/SelectUserSettingsEntity.java create mode 100644 solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/UpsertCloudDatumStreamSettingsEntity.java create mode 100644 solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/UpsertUserSettingsEntity.java create mode 100644 solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/BasicCloudDatumStreamSettings.java create mode 100644 solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/CloudDatumStreamSettings.java create mode 100644 solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/CloudDatumStreamSettingsEntity.java create mode 100644 solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/UserSettingsEntity.java create mode 100644 solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/dao/jdbc/test/JdbcCloudDatumStreamSettingsEntityDaoTests.java create mode 100644 solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/dao/jdbc/test/JdbcUserSettingsEntityDaoTests.java create mode 100644 solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/domain/CloudDatumStreamSettingsEntityInput.java create mode 100644 solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/domain/UserSettingsEntityInput.java diff --git a/solarnet-db-setup/postgres/updates/NET-414-cloud-integrations-flux.sql b/solarnet-db-setup/postgres/updates/NET-414-cloud-integrations-flux.sql new file mode 100644 index 000000000..0dff0ce14 --- /dev/null +++ b/solarnet-db-setup/postgres/updates/NET-414-cloud-integrations-flux.sql @@ -0,0 +1,44 @@ +/** + * Cloud integration user (account) configuration. + * + * @column user_id the ID of the account owner + * @column id the ID of the configuration + * @column created the creation date + * @column modified the modification date + * @column pub_in a flag to publish datum streams to SolarIn + * @column pub_flux a flag to publish datum streams to SolarFlux + */ +CREATE TABLE solardin.cin_user_settings ( + user_id BIGINT NOT NULL, + created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + modified TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + pub_in BOOLEAN NOT NULL DEFAULT TRUE, + pub_flux BOOLEAN NOT NULL DEFAULT FALSE, + CONSTRAINT cin_user_settings_pk PRIMARY KEY (user_id), + CONSTRAINT cin_user_settings_user_fk FOREIGN KEY (user_id) + REFERENCES solaruser.user_user (id) MATCH SIMPLE + ON UPDATE NO ACTION ON DELETE CASCADE +); + +/** + * Cloud datum stream settings, to override cin_user_settings. + * + * @column user_id the ID of the account owner + * @column ds_id the ID of the datum stream associated with this configuration + * @column created the creation date + * @column modified the modification date + * @column pub_in a flag to publish datum streams to SolarIn + * @column pub_flux a flag to publish datum streams to SolarFlux + */ +CREATE TABLE solardin.cin_datum_stream_settings ( + user_id BIGINT NOT NULL, + ds_id BIGINT NOT NULL, + created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + modified TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + pub_in BOOLEAN NOT NULL DEFAULT TRUE, + pub_flux BOOLEAN NOT NULL DEFAULT TRUE, + CONSTRAINT cin_datum_stream_settings_pk PRIMARY KEY (user_id, ds_id), + CONSTRAINT cin_datum_stream_settings_ds_fk FOREIGN KEY (user_id, ds_id) + REFERENCES solardin.cin_datum_stream (user_id, id) MATCH SIMPLE + ON UPDATE NO ACTION ON DELETE CASCADE +); diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/DaoCloudDatumStreamPollService.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/DaoCloudDatumStreamPollService.java index b718ec663..252b55c09 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/DaoCloudDatumStreamPollService.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/DaoCloudDatumStreamPollService.java @@ -52,9 +52,12 @@ import net.solarnetwork.central.c2c.biz.CloudDatumStreamService; import net.solarnetwork.central.c2c.dao.CloudDatumStreamConfigurationDao; import net.solarnetwork.central.c2c.dao.CloudDatumStreamPollTaskDao; +import net.solarnetwork.central.c2c.dao.CloudDatumStreamSettingsEntityDao; +import net.solarnetwork.central.c2c.domain.BasicCloudDatumStreamSettings; import net.solarnetwork.central.c2c.domain.BasicQueryFilter; import net.solarnetwork.central.c2c.domain.CloudDatumStreamConfiguration; import net.solarnetwork.central.c2c.domain.CloudDatumStreamPollTaskEntity; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettings; import net.solarnetwork.central.c2c.domain.CloudIntegrationsUserEvents; import net.solarnetwork.central.dao.SolarNodeOwnershipDao; import net.solarnetwork.central.datum.domain.GeneralObjectDatum; @@ -70,7 +73,7 @@ * DAO based implementation of {@link CloudDatumStreamPollService}. * * @author matt - * @version 1.2 + * @version 1.3 */ public class DaoCloudDatumStreamPollService implements CloudDatumStreamPollService, ServiceLifecycleObserver, CloudIntegrationsUserEvents { @@ -78,6 +81,10 @@ public class DaoCloudDatumStreamPollService /** The {@code shutdownMaxWait} property default value: 1 minute. */ public static final Duration DEFAULT_SHUTDOWN_MAX_WAIT = Duration.ofMinutes(1); + /** The {@code defaultDatumStreamSettings} default value. */ + public static final CloudDatumStreamSettings DEFAULT_DATUM_STREAM_SETTINGS = new BasicCloudDatumStreamSettings( + true, false); + private final Logger log = LoggerFactory.getLogger(getClass()); private final Clock clock; @@ -85,10 +92,12 @@ public class DaoCloudDatumStreamPollService private final SolarNodeOwnershipDao nodeOwnershipDao; private final CloudDatumStreamPollTaskDao taskDao; private final CloudDatumStreamConfigurationDao datumStreamDao; + private final CloudDatumStreamSettingsEntityDao datumStreamSettingsDao; private final DatumWriteOnlyDao datumDao; private final ExecutorService executorService; private final Function datumStreamServiceProvider; private Duration shutdownMaxWait = DEFAULT_SHUTDOWN_MAX_WAIT; + private CloudDatumStreamSettings defaultDatumStreamSettings = DEFAULT_DATUM_STREAM_SETTINGS; /** * Constructor. @@ -103,6 +112,8 @@ public class DaoCloudDatumStreamPollService * the task DAO * @param datumStreamDao * the datum stream DAO + * @param datumStreamSettingsDao + * the datum stream settings DAO * @param datumDao * the datum DAO * @param executor @@ -116,7 +127,8 @@ public class DaoCloudDatumStreamPollService */ public DaoCloudDatumStreamPollService(Clock clock, UserEventAppenderBiz userEventAppenderBiz, SolarNodeOwnershipDao nodeOwnershipDao, CloudDatumStreamPollTaskDao taskDao, - CloudDatumStreamConfigurationDao datumStreamDao, DatumWriteOnlyDao datumDao, + CloudDatumStreamConfigurationDao datumStreamDao, + CloudDatumStreamSettingsEntityDao datumStreamSettingsDao, DatumWriteOnlyDao datumDao, ExecutorService executor, Function datumStreamServiceProvider) { super(); @@ -125,6 +137,8 @@ public DaoCloudDatumStreamPollService(Clock clock, UserEventAppenderBiz userEven this.nodeOwnershipDao = requireNonNullArgument(nodeOwnershipDao, "nodeOwnershipDao"); this.taskDao = requireNonNullArgument(taskDao, "taskDao"); this.datumStreamDao = requireNonNullArgument(datumStreamDao, "datumStreamDao"); + this.datumStreamSettingsDao = requireNonNullArgument(datumStreamSettingsDao, + "datumStreamSettingsDao"); this.datumDao = requireNonNullArgument(datumDao, "datumDao"); this.executorService = requireNonNullArgument(executor, "executor"); this.datumStreamServiceProvider = requireNonNullArgument(datumStreamServiceProvider, @@ -247,6 +261,9 @@ private CloudDatumStreamPollTaskEntity executeTask() throws Exception { return taskInfo; } + final CloudDatumStreamSettings datumStreamSettings = datumStreamSettingsDao.resolveSettings( + datumStream.getUserId(), datumStream.getConfigId(), defaultDatumStreamSettings); + final String datumStreamIdent = datumStream.getId().ident(); if ( !datumStream.isFullyConfigured() ) { @@ -336,11 +353,17 @@ private CloudDatumStreamPollTaskEntity executeTask() throws Exception { polledDatum.size()); for ( var datum : polledDatum ) { if ( datum instanceof DatumEntity d ) { - datumDao.store(d); + if ( datumStreamSettings.isPublishToSolarIn() ) { + datumDao.store(d); + } } else if ( datum instanceof GeneralObjectDatum d ) { - datumDao.persist(d); + if ( datumStreamSettings.isPublishToSolarIn() ) { + datumDao.persist(d); + } } else { - datumDao.store(datum); + if ( datumStreamSettings.isPublishToSolarIn() ) { + datumDao.store(datum); + } } if ( lastDatumDate == null || lastDatumDate.isBefore(datum.getTimestamp()) ) { lastDatumDate = datum.getTimestamp(); @@ -434,4 +457,29 @@ public final void setShutdownMaxWait(Duration shutdownMaxWait) { this.shutdownMaxWait = (shutdownMaxWait != null ? shutdownMaxWait : DEFAULT_SHUTDOWN_MAX_WAIT); } + /** + * Get the default datum stream settings. + * + * @return the settings, never {@literal null} + * @since 1.3 + */ + public final CloudDatumStreamSettings getDefaultDatumStreamSettings() { + return defaultDatumStreamSettings; + } + + /** + * Set the default datum stream settings. + * + * @param defaultDatumStreamSettings + * the settings to set; if {@code null} then + * {@link #DEFAULT_DATUM_STREAM_SETTINGS} will be used + * @since 1.3 + */ + public final void setDefaultDatumStreamSettings( + CloudDatumStreamSettings defaultDatumStreamSettings) { + this.defaultDatumStreamSettings = (defaultDatumStreamSettings != null + ? defaultDatumStreamSettings + : DEFAULT_DATUM_STREAM_SETTINGS); + } + } diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/CloudIntegrationsDaoConfig.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/CloudIntegrationsDaoConfig.java index 57011d152..ccd51ada5 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/CloudIntegrationsDaoConfig.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/CloudIntegrationsDaoConfig.java @@ -32,18 +32,22 @@ import net.solarnetwork.central.c2c.dao.CloudDatumStreamMappingConfigurationDao; import net.solarnetwork.central.c2c.dao.CloudDatumStreamPollTaskDao; import net.solarnetwork.central.c2c.dao.CloudDatumStreamPropertyConfigurationDao; +import net.solarnetwork.central.c2c.dao.CloudDatumStreamSettingsEntityDao; import net.solarnetwork.central.c2c.dao.CloudIntegrationConfigurationDao; +import net.solarnetwork.central.c2c.dao.UserSettingsEntityDao; import net.solarnetwork.central.c2c.dao.jdbc.JdbcCloudDatumStreamConfigurationDao; import net.solarnetwork.central.c2c.dao.jdbc.JdbcCloudDatumStreamMappingConfigurationDao; import net.solarnetwork.central.c2c.dao.jdbc.JdbcCloudDatumStreamPollTaskDao; import net.solarnetwork.central.c2c.dao.jdbc.JdbcCloudDatumStreamPropertyConfigurationDao; +import net.solarnetwork.central.c2c.dao.jdbc.JdbcCloudDatumStreamSettingsEntityDao; import net.solarnetwork.central.c2c.dao.jdbc.JdbcCloudIntegrationConfigurationDao; +import net.solarnetwork.central.c2c.dao.jdbc.JdbcUserSettingsEntityDao; /** * Cloud integrations DAO configuration. * * @author matt - * @version 1.1 + * @version 1.2 */ @Configuration(proxyBeanMethods = false) @Profile(CLOUD_INTEGRATIONS) @@ -58,7 +62,7 @@ public class CloudIntegrationsDaoConfig { * @return the DAO */ @Bean - public CloudIntegrationConfigurationDao cloudIntegrationConfigurationConfigurationDao() { + public CloudIntegrationConfigurationDao cloudIntegrationConfigurationDao() { return new JdbcCloudIntegrationConfigurationDao(jdbcOperations); } @@ -68,7 +72,7 @@ public CloudIntegrationConfigurationDao cloudIntegrationConfigurationConfigurati * @return the DAO */ @Bean - public CloudDatumStreamConfigurationDao cloudDatumStreamConfigurationConfigurationDao() { + public CloudDatumStreamConfigurationDao cloudDatumStreamConfigurationDao() { return new JdbcCloudDatumStreamConfigurationDao(jdbcOperations); } @@ -78,7 +82,7 @@ public CloudDatumStreamConfigurationDao cloudDatumStreamConfigurationConfigurati * @return the DAO */ @Bean - public CloudDatumStreamMappingConfigurationDao cloudDatumStreamMappingConfigurationConfigurationDao() { + public CloudDatumStreamMappingConfigurationDao cloudDatumStreamMappingConfigurationDao() { return new JdbcCloudDatumStreamMappingConfigurationDao(jdbcOperations); } @@ -88,7 +92,7 @@ public CloudDatumStreamMappingConfigurationDao cloudDatumStreamMappingConfigurat * @return the DAO */ @Bean - public CloudDatumStreamPropertyConfigurationDao cloudDatumStreamPropertyConfigurationConfigurationDao() { + public CloudDatumStreamPropertyConfigurationDao cloudDatumStreamPropertyConfigurationDao() { return new JdbcCloudDatumStreamPropertyConfigurationDao(jdbcOperations); } @@ -102,4 +106,26 @@ public CloudDatumStreamPollTaskDao cloudDatumStreamPollTaskDaoDao() { return new JdbcCloudDatumStreamPollTaskDao(jdbcOperations); } + /** + * The user settings DAO. + * + * @return the DAO + * @since 1.2 + */ + @Bean + public UserSettingsEntityDao cloudIntegrationsUserSettingsEntityDao() { + return new JdbcUserSettingsEntityDao(jdbcOperations); + } + + /** + * The cloud datum stream settings DAO. + * + * @return the DAO + * @since 1.2 + */ + @Bean + public CloudDatumStreamSettingsEntityDao cloudDatumStreamSettingsEntityDao() { + return new JdbcCloudDatumStreamSettingsEntityDao(jdbcOperations); + } + } diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/BasicFilter.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/BasicFilter.java index a0cab7b0d..0f3593561 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/BasicFilter.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/BasicFilter.java @@ -39,11 +39,11 @@ * Basic implementation of cloud integration query filter. * * @author matt - * @version 1.0 + * @version 1.1 */ public class BasicFilter extends BasicCoreCriteria implements CloudIntegrationFilter, CloudDatumStreamFilter, CloudDatumStreamMappingFilter, - CloudDatumStreamPropertyFilter, CloudDatumStreamPollTaskFilter { + CloudDatumStreamPropertyFilter, CloudDatumStreamPollTaskFilter, CloudDatumStreamSettingsFilter { private Long[] integrationIds; private Long[] datumStreamIds; diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/CloudDatumStreamSettingsEntityDao.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/CloudDatumStreamSettingsEntityDao.java new file mode 100644 index 000000000..197934ce8 --- /dev/null +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/CloudDatumStreamSettingsEntityDao.java @@ -0,0 +1,57 @@ +/* ================================================================== + * CloudDatumStreamSettingsEntityDao.java - 28/10/2024 7:21:50 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.dao; + +import net.solarnetwork.central.c2c.domain.CloudDatumStreamConfiguration; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettings; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettingsEntity; +import net.solarnetwork.central.c2c.domain.UserSettingsEntity; +import net.solarnetwork.central.common.dao.GenericCompositeKey2Dao; +import net.solarnetwork.central.domain.UserLongCompositePK; +import net.solarnetwork.dao.FilterableDao; + +/** + * DAO API for {@link CloudDatumStreamSettingsEntity} entities. + * + * @author matt + * @version 1.0 + */ +public interface CloudDatumStreamSettingsEntityDao + extends GenericCompositeKey2Dao, + FilterableDao { + + /** + * Get settings resolved using {@link UserSettingsEntity} defaults if a + * datum stream setting does not exist. + * + * @param userId + * the owner user ID + * @param datumStreamId + * the {@link CloudDatumStreamConfiguration} ID to resolve settings + * for + * @return the settings, or {@code defaultSettings} if no datum stream or + * user settings exist + */ + CloudDatumStreamSettings resolveSettings(Long userId, Long datumStreamId, + CloudDatumStreamSettings defaultSettings); +} diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/CloudDatumStreamSettingsFilter.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/CloudDatumStreamSettingsFilter.java new file mode 100644 index 000000000..515f40bf0 --- /dev/null +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/CloudDatumStreamSettingsFilter.java @@ -0,0 +1,34 @@ +/* ================================================================== + * CloudDatumStreamSettingsFilter.java - 28/10/2024 8:11:33 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.dao; + +/** + * A filter for cloud datum stream settings entities. + * + * @author matt + * @version 1.0 + */ +public interface CloudDatumStreamSettingsFilter + extends CloudIntegrationsFilter, CloudDatumStreamCriteria { + +} diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/UserSettingsEntityDao.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/UserSettingsEntityDao.java new file mode 100644 index 000000000..7e61ca8db --- /dev/null +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/UserSettingsEntityDao.java @@ -0,0 +1,36 @@ +/* ================================================================== + * UserSettingsEntityDao.java - 28/10/2024 7:21:50 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.dao; + +import net.solarnetwork.central.c2c.domain.UserSettingsEntity; +import net.solarnetwork.central.dao.GenericDao; + +/** + * DAO API for {@link UserSettingsEntity} entities. + * + * @author matt + * @version 1.0 + */ +public interface UserSettingsEntityDao extends GenericDao { + +} diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/CloudDatumStreamSettingsEntityRowMapper.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/CloudDatumStreamSettingsEntityRowMapper.java new file mode 100644 index 000000000..cbd22f77b --- /dev/null +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/CloudDatumStreamSettingsEntityRowMapper.java @@ -0,0 +1,90 @@ +/* ================================================================== + * CloudDatumStreamSettingsEntityRowMapper.java - 28/10/2024 7:54:26 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.dao.jdbc; + +import static net.solarnetwork.central.common.dao.jdbc.sql.CommonJdbcUtils.getTimestampInstant; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Instant; +import org.springframework.jdbc.core.RowMapper; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettingsEntity; + +/** + * Row mapper for {@link CloudDatumStreamSettingsEntity} entities. + * + *

+ * The expected column order in the SQL results is: + *

+ * + *
    + *
  1. user_id (BIGINT)
  2. + *
  3. ds_id (BIGINT)
  4. + *
  5. created (TIMESTAMP)
  6. + *
  7. modified (TIMESTAMP)
  8. + *
  9. pub_in (BOOLEAN)
  10. + *
  11. pub_flux (BOOLEAN)
  12. + *
+ * + * @author matt + * @version 1.0 + */ +public class CloudDatumStreamSettingsEntityRowMapper + implements RowMapper { + + /** A default instance. */ + public static final RowMapper INSTANCE = new CloudDatumStreamSettingsEntityRowMapper(); + + private final int columnOffset; + + /** + * Default constructor. + */ + public CloudDatumStreamSettingsEntityRowMapper() { + this(0); + } + + /** + * Constructor. + * + * @param columnOffset + * a column offset to apply + */ + public CloudDatumStreamSettingsEntityRowMapper(int columnOffset) { + this.columnOffset = columnOffset; + } + + @Override + public CloudDatumStreamSettingsEntity mapRow(ResultSet rs, int rowNum) throws SQLException { + int p = columnOffset; + Long userId = rs.getObject(++p, Long.class); + Long datumStreamId = rs.getObject(++p, Long.class); + Instant ts = getTimestampInstant(rs, ++p); + CloudDatumStreamSettingsEntity conf = new CloudDatumStreamSettingsEntity(userId, datumStreamId, + ts); + conf.setModified(getTimestampInstant(rs, ++p)); + conf.setPublishToSolarIn(rs.getBoolean(++p)); + conf.setPublishToSolarFlux(rs.getBoolean(++p)); + return conf; + } + +} diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/JdbcCloudDatumStreamSettingsEntityDao.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/JdbcCloudDatumStreamSettingsEntityDao.java new file mode 100644 index 000000000..1f7c3c95b --- /dev/null +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/JdbcCloudDatumStreamSettingsEntityDao.java @@ -0,0 +1,151 @@ +/* ================================================================== + * JdbcCloudDatumStreamSettingsEntityDao.java - 28/10/2024 10:06:28 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.dao.jdbc; + +import static java.time.Instant.now; +import static java.util.stream.StreamSupport.stream; +import static net.solarnetwork.central.common.dao.jdbc.sql.CommonJdbcUtils.executeFilterQuery; +import static net.solarnetwork.util.ObjectUtils.requireNonNullArgument; +import java.util.Collection; +import java.util.List; +import org.springframework.jdbc.core.JdbcOperations; +import net.solarnetwork.central.c2c.dao.BasicFilter; +import net.solarnetwork.central.c2c.dao.CloudDatumStreamSettingsEntityDao; +import net.solarnetwork.central.c2c.dao.CloudDatumStreamSettingsFilter; +import net.solarnetwork.central.c2c.dao.jdbc.sql.SelectCloudDatumStreamSettingsEntity; +import net.solarnetwork.central.c2c.dao.jdbc.sql.UpsertCloudDatumStreamSettingsEntity; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettings; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettingsEntity; +import net.solarnetwork.central.common.dao.jdbc.sql.DeleteForCompositeKey; +import net.solarnetwork.central.domain.UserLongCompositePK; +import net.solarnetwork.dao.FilterResults; +import net.solarnetwork.domain.SortDescriptor; + +/** + * JDBC implementation of {@link CloudDatumStreamSettingsEntityDao}. + * + * @author matt + * @version 1.0 + */ +public class JdbcCloudDatumStreamSettingsEntityDao implements CloudDatumStreamSettingsEntityDao { + + private final JdbcOperations jdbcOps; + + /** + * Constructor. + * + * @param jdbcOps + * the JDBC operations + * @throws IllegalArgumentException + * if any argument is {@literal null} + */ + public JdbcCloudDatumStreamSettingsEntityDao(JdbcOperations jdbcOps) { + super(); + this.jdbcOps = requireNonNullArgument(jdbcOps, "jdbcOps"); + } + + @Override + public Class getObjectType() { + return CloudDatumStreamSettingsEntity.class; + } + + @Override + public CloudDatumStreamSettingsEntity entityKey(UserLongCompositePK id) { + return new CloudDatumStreamSettingsEntity(id, now()); + } + + @Override + public UserLongCompositePK create(Long userId, CloudDatumStreamSettingsEntity entity) { + requireNonNullArgument(entity, "entity"); + final var sql = new UpsertCloudDatumStreamSettingsEntity(userId, entity.getDatumStreamId(), + entity); + int count = jdbcOps.update(sql); + return (count > 0 ? new UserLongCompositePK(userId, entity.getDatumStreamId()) : null); + } + + @Override + public Collection findAll(Long userId, List sorts) { + var filter = new BasicFilter(); + filter.setUserId(requireNonNullArgument(userId, "userId")); + var results = findFiltered(filter, sorts, null, null); + return stream(results.spliterator(), false).toList(); + } + + @Override + public FilterResults findFiltered( + CloudDatumStreamSettingsFilter filter, List sorts, Integer offset, + Integer max) { + requireNonNullArgument(requireNonNullArgument(filter, "filter").getUserId(), "filter.userId"); + var sql = new SelectCloudDatumStreamSettingsEntity(filter); + return executeFilterQuery(jdbcOps, filter, sql, + CloudDatumStreamSettingsEntityRowMapper.INSTANCE); + } + + @Override + public UserLongCompositePK save(CloudDatumStreamSettingsEntity entity) { + requireNonNullArgument(entity, "entity"); + return create(entity.getUserId(), entity); + } + + @Override + public CloudDatumStreamSettingsEntity get(UserLongCompositePK id) { + var filter = new BasicFilter(); + filter.setUserId( + requireNonNullArgument(requireNonNullArgument(id, "id").getUserId(), "id.userId")); + filter.setDatumStreamId(requireNonNullArgument(id.getEntityId(), "id.entityId")); + var sql = new SelectCloudDatumStreamSettingsEntity(filter); + var results = executeFilterQuery(jdbcOps, filter, sql, + CloudDatumStreamSettingsEntityRowMapper.INSTANCE); + return stream(results.spliterator(), false).findFirst().orElse(null); + } + + @Override + public Collection getAll(List sorts) { + throw new UnsupportedOperationException(); + } + + private static final String TABLE_NAME = "solardin.cin_datum_stream_settings"; + private static final String ID_COLUMN_NAME = "ds_id"; + private static final String[] PK_COLUMN_NAMES = new String[] { "user_id", ID_COLUMN_NAME }; + + @Override + public void delete(CloudDatumStreamSettingsEntity entity) { + DeleteForCompositeKey sql = new DeleteForCompositeKey( + requireNonNullArgument(entity, "entity").getId(), TABLE_NAME, PK_COLUMN_NAMES); + jdbcOps.update(sql); + } + + @Override + public CloudDatumStreamSettings resolveSettings(Long userId, Long datumStreamId, + CloudDatumStreamSettings defaultSettings) { + var filter = new BasicFilter(); + filter.setUserId(requireNonNullArgument(userId, "userId")); + filter.setDatumStreamId(requireNonNullArgument(datumStreamId, "datumStreamId")); + var sql = new SelectCloudDatumStreamSettingsEntity(filter, true); + var results = executeFilterQuery(jdbcOps, filter, sql, + CloudDatumStreamSettingsEntityRowMapper.INSTANCE); + return stream(results.spliterator(), false).findFirst().map(CloudDatumStreamSettings.class::cast) + .orElse(defaultSettings); + } + +} diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/JdbcUserSettingsEntityDao.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/JdbcUserSettingsEntityDao.java new file mode 100644 index 000000000..c3399fef3 --- /dev/null +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/JdbcUserSettingsEntityDao.java @@ -0,0 +1,91 @@ +/* ================================================================== + * JdbcUserSettingsEntityDao.java - 28/10/2024 7:34:16 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.dao.jdbc; + +import static net.solarnetwork.util.ObjectUtils.requireNonNullArgument; +import java.util.List; +import org.springframework.jdbc.core.JdbcOperations; +import net.solarnetwork.central.c2c.dao.UserSettingsEntityDao; +import net.solarnetwork.central.c2c.dao.jdbc.sql.DeleteUserSettingsEntity; +import net.solarnetwork.central.c2c.dao.jdbc.sql.SelectUserSettingsEntity; +import net.solarnetwork.central.c2c.dao.jdbc.sql.UpsertUserSettingsEntity; +import net.solarnetwork.central.c2c.domain.UserSettingsEntity; +import net.solarnetwork.domain.SortDescriptor; + +/** + * JDBC implementation of {@link UserSettingsEntityDao}. + * + * @author matt + * @version 1.0 + */ +public class JdbcUserSettingsEntityDao implements UserSettingsEntityDao { + + private final JdbcOperations jdbcOps; + + /** + * Constructor. + * + * @param jdbcOps + * the JDBC operations + * @throws IllegalArgumentException + * if any argument is {@literal null} + */ + public JdbcUserSettingsEntityDao(JdbcOperations jdbcOps) { + super(); + this.jdbcOps = requireNonNullArgument(jdbcOps, "jdbcOps"); + } + + @Override + public Class getObjectType() { + return UserSettingsEntity.class; + } + + @Override + public Long store(UserSettingsEntity entity) { + Long userId = requireNonNullArgument(requireNonNullArgument(entity, "entity").getUserId(), + "entity.userId"); + final var sql = new UpsertUserSettingsEntity(userId, entity); + int count = jdbcOps.update(sql); + return (count > 0 ? userId : null); + } + + @Override + public UserSettingsEntity get(Long id) { + var sql = new SelectUserSettingsEntity(id); + List results = jdbcOps.query(sql, UserSettingsEntityRowMapper.INSTANCE); + return (results != null && !results.isEmpty() ? results.getFirst() : null); + } + + @Override + public List getAll(List sortDescriptors) { + throw new UnsupportedOperationException(); + } + + @Override + public void delete(UserSettingsEntity entity) { + var sql = new DeleteUserSettingsEntity(requireNonNullArgument( + requireNonNullArgument(entity, "entity").getUserId(), "entity.userId")); + jdbcOps.update(sql); + } + +} diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/UserSettingsEntityRowMapper.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/UserSettingsEntityRowMapper.java new file mode 100644 index 000000000..81d126d68 --- /dev/null +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/UserSettingsEntityRowMapper.java @@ -0,0 +1,86 @@ +/* ================================================================== + * UserSettingsEntityRowMapper.java - 28/10/2024 7:54:26 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.dao.jdbc; + +import static net.solarnetwork.central.common.dao.jdbc.sql.CommonJdbcUtils.getTimestampInstant; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Instant; +import org.springframework.jdbc.core.RowMapper; +import net.solarnetwork.central.c2c.domain.UserSettingsEntity; + +/** + * Row mapper for {@link UserSettingsEntity} entities. + * + *

+ * The expected column order in the SQL results is: + *

+ * + *
    + *
  1. user_id (BIGINT)
  2. + *
  3. created (TIMESTAMP)
  4. + *
  5. modified (TIMESTAMP)
  6. + *
  7. pub_in (BOOLEAN)
  8. + *
  9. pub_flux (BOOLEAN)
  10. + *
+ * + * @author matt + * @version 1.0 + */ +public class UserSettingsEntityRowMapper implements RowMapper { + + /** A default instance. */ + public static final RowMapper INSTANCE = new UserSettingsEntityRowMapper(); + + private final int columnOffset; + + /** + * Default constructor. + */ + public UserSettingsEntityRowMapper() { + this(0); + } + + /** + * Constructor. + * + * @param columnOffset + * a column offset to apply + */ + public UserSettingsEntityRowMapper(int columnOffset) { + this.columnOffset = columnOffset; + } + + @Override + public UserSettingsEntity mapRow(ResultSet rs, int rowNum) throws SQLException { + int p = columnOffset; + Long userId = rs.getObject(++p, Long.class); + Instant ts = getTimestampInstant(rs, ++p); + UserSettingsEntity conf = new UserSettingsEntity(userId, ts); + conf.setModified(getTimestampInstant(rs, ++p)); + conf.setPublishToSolarIn(rs.getBoolean(++p)); + conf.setPublishToSolarFlux(rs.getBoolean(++p)); + return conf; + } + +} diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/DeleteUserSettingsEntity.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/DeleteUserSettingsEntity.java new file mode 100644 index 000000000..6fb48d677 --- /dev/null +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/DeleteUserSettingsEntity.java @@ -0,0 +1,71 @@ +/* ================================================================== + * SelectUserSettingsEntity.java - 28/10/2024 7:49:22 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.dao.jdbc.sql; + +import static net.solarnetwork.util.ObjectUtils.requireNonNullArgument; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.SqlProvider; +import net.solarnetwork.central.c2c.domain.UserSettingsEntity; + +/** + * Support for DELETE for {@link UserSettingsEntity} entities. + * + * @author matt + * @version 1.0 + */ +public class DeleteUserSettingsEntity implements PreparedStatementCreator, SqlProvider { + + private static final String SQL = """ + DELETE FROM solardin.cin_user_settings + WHERE user_id = ? + """; + + private final Long userId; + + /** + * Constructor. + * + * @param filter + * the filter + */ + public DeleteUserSettingsEntity(Long userId) { + super(); + this.userId = requireNonNullArgument(userId, "userId"); + } + + @Override + public String getSql() { + return SQL; + } + + @Override + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + PreparedStatement stmt = con.prepareStatement(getSql()); + stmt.setObject(1, userId); + return stmt; + } + +} diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/SelectCloudDatumStreamSettingsEntity.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/SelectCloudDatumStreamSettingsEntity.java new file mode 100644 index 000000000..2d11d07c7 --- /dev/null +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/SelectCloudDatumStreamSettingsEntity.java @@ -0,0 +1,245 @@ +/* ================================================================== + * SelectCloudIntegrationConfiguration.java - 2/10/2024 8:46:14 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.dao.jdbc.sql; + +import static net.solarnetwork.central.common.dao.jdbc.sql.CommonSqlUtils.prepareOptimizedArrayParameter; +import static net.solarnetwork.central.common.dao.jdbc.sql.CommonSqlUtils.whereOptimizedArrayContains; +import static net.solarnetwork.util.ObjectUtils.requireNonNullArgument; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.SqlProvider; +import net.solarnetwork.central.c2c.dao.CloudDatumStreamSettingsFilter; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettingsEntity; +import net.solarnetwork.central.common.dao.jdbc.CountPreparedStatementCreatorProvider; +import net.solarnetwork.central.common.dao.jdbc.sql.CommonSqlUtils; + +/** + * Support for SELECT for {@link CloudDatumStreamSettingsEntity} entities. + * + * @author matt + * @version 1.0 + */ +public class SelectCloudDatumStreamSettingsEntity + implements PreparedStatementCreator, SqlProvider, CountPreparedStatementCreatorProvider { + + /** The {@code fetchSize} property default value. */ + public static final int DEFAULT_FETCH_SIZE = 1000; + + private final CloudDatumStreamSettingsFilter filter; + private final boolean resolveUserSettings; + private final int fetchSize; + + /** + * Constructor. + * + *

+ * The {@link #DEFAULT_FETCH_SIZE} will be used and + * {@code resolveUserSettings} will be {@code false}. + *

+ * + * @param filter + * the filter + * @throws IllegalArgumentException + * if any argument is {@code null} + */ + public SelectCloudDatumStreamSettingsEntity(CloudDatumStreamSettingsFilter filter) { + this(filter, false, DEFAULT_FETCH_SIZE); + } + + /** + * Constructor. + * + *

+ * The {@link #DEFAULT_FETCH_SIZE} will be used. + *

+ * + * @param filter + * the filter + * @param resolveUserSettings + * {@code true} to resolve user settings as default values; only + * honored if the {@code filter} also provides a user ID and datum + * stream ID + * @throws IllegalArgumentException + * if any argument is {@code null} + */ + public SelectCloudDatumStreamSettingsEntity(CloudDatumStreamSettingsFilter filter, + boolean resolveUserSettings) { + this(filter, resolveUserSettings, DEFAULT_FETCH_SIZE); + } + + /** + * Constructor. + * + * @param filter + * the filter + * @param resolveUserSettings + * {@code true} to resolve user settings as default values; only + * honored if the {@code filter} also provides a user ID and datum + * stream ID + * @param fetchSize + * the fetch size + * @throws IllegalArgumentException + * if any argument is {@code null} + */ + public SelectCloudDatumStreamSettingsEntity(CloudDatumStreamSettingsFilter filter, + boolean resolveUserSettings, int fetchSize) { + super(); + this.filter = requireNonNullArgument(filter, "filter"); + this.resolveUserSettings = resolveUserSettings && filter.hasUserCriteria() + && filter.hasDatumStreamCriteria(); + this.fetchSize = fetchSize; + } + + @Override + public String getSql() { + StringBuilder buf = new StringBuilder(); + sqlCore(buf); + sqlWhere(buf); + if ( !resolveUserSettings ) { + sqlOrderBy(buf); + } + CommonSqlUtils.limitOffset(filter, buf); + return buf.toString(); + } + + private void sqlCore(StringBuilder buf) { + if ( resolveUserSettings ) { + buf.append(""" + WITH cdss AS ( + SELECT user_id + , ds_id + , created + , modified + , pub_in + , pub_flux + FROM solardin.cin_datum_stream_settings + WHERE user_id = ? + AND ds_id = ? + + UNION ALL + + SELECT user_id + , x'8000000000000000'::BIGINT AS ds_id + , created + , modified + , pub_in + , pub_flux + FROM solardin.cin_user_settings + WHERE user_id = ? + ) + SELECT user_id + , ds_id + , created + , modified + , pub_in + , pub_flux + FROM cdss + LIMIT 1 + """); + } else { + buf.append(""" + SELECT cdss.user_id + , cdss.ds_id + , cdss.created + , cdss.modified + , cdss.pub_in + , cdss.pub_flux + FROM solardin.cin_datum_stream_settings cdss + """); + } + } + + private void sqlWhere(StringBuilder buf) { + if ( resolveUserSettings ) { + return; + } + StringBuilder where = new StringBuilder(); + int idx = 0; + if ( filter.hasUserCriteria() ) { + idx += whereOptimizedArrayContains(filter.getUserIds(), "cdss.user_id", where); + } + if ( filter.hasDatumStreamCriteria() ) { + idx += whereOptimizedArrayContains(filter.getDatumStreamIds(), "cdss.ds_id", where); + } + if ( idx > 0 ) { + buf.append("WHERE").append(where.substring(4)); + } + } + + private void sqlOrderBy(StringBuilder buf) { + buf.append("ORDER BY cdss.user_id, cdss.ds_id"); + } + + @Override + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + PreparedStatement stmt = con.prepareStatement(getSql(), ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT); + int p = prepareCore(con, stmt, 0); + CommonSqlUtils.prepareLimitOffset(filter, con, stmt, p); + if ( fetchSize > 0 ) { + stmt.setFetchSize(fetchSize); + } + return stmt; + } + + private int prepareCore(Connection con, PreparedStatement stmt, int p) throws SQLException { + if ( filter.hasUserCriteria() ) { + p = prepareOptimizedArrayParameter(con, stmt, p, filter.getUserIds()); + } + if ( filter.hasDatumStreamCriteria() ) { + p = prepareOptimizedArrayParameter(con, stmt, p, filter.getDatumStreamIds()); + } + if ( resolveUserSettings ) { + p = prepareOptimizedArrayParameter(con, stmt, p, filter.getUserIds()); + } + return p; + } + + @Override + public PreparedStatementCreator countPreparedStatementCreator() { + return new CountPreparedStatementCreator(); + } + + private final class CountPreparedStatementCreator implements PreparedStatementCreator, SqlProvider { + + @Override + public String getSql() { + StringBuilder buf = new StringBuilder(); + sqlCore(buf); + sqlWhere(buf); + return CommonSqlUtils.wrappedCountQuery(buf.toString()); + } + + @Override + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + PreparedStatement stmt = con.prepareStatement(getSql()); + prepareCore(con, stmt, 0); + return stmt; + } + + } + +} diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/SelectUserSettingsEntity.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/SelectUserSettingsEntity.java new file mode 100644 index 000000000..e287e7f48 --- /dev/null +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/SelectUserSettingsEntity.java @@ -0,0 +1,74 @@ +/* ================================================================== + * SelectUserSettingsEntity.java - 28/10/2024 7:49:22 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.dao.jdbc.sql; + +import static net.solarnetwork.util.ObjectUtils.requireNonNullArgument; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.SqlProvider; +import net.solarnetwork.central.c2c.domain.UserSettingsEntity; + +/** + * Support for SELECT for {@link UserSettingsEntity} entities. + * + * @author matt + * @version 1.0 + */ +public class SelectUserSettingsEntity implements PreparedStatementCreator, SqlProvider { + + private static final String SQL = """ + SELECT user_id,created,modified,pub_in,pub_flux + FROM solardin.cin_user_settings + WHERE user_id = ? + """; + + private final Long userId; + + /** + * Constructor. + * + * @param filter + * the filter + */ + public SelectUserSettingsEntity(Long userId) { + super(); + this.userId = requireNonNullArgument(userId, "userId"); + } + + @Override + public String getSql() { + return SQL; + } + + @Override + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + PreparedStatement stmt = con.prepareStatement(getSql(), ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT); + stmt.setObject(1, userId); + return stmt; + } + +} diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/UpsertCloudDatumStreamSettingsEntity.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/UpsertCloudDatumStreamSettingsEntity.java new file mode 100644 index 000000000..de6d2fb6b --- /dev/null +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/UpsertCloudDatumStreamSettingsEntity.java @@ -0,0 +1,100 @@ +/* ================================================================== + * UpsertCloudDatumStreamSettingsEntity.java - 28/10/2024 7:38:26 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.dao.jdbc.sql; + +import static net.solarnetwork.util.ObjectUtils.requireNonNullArgument; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.Instant; +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.SqlProvider; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettingsEntity; + +/** + * Support for INSERT ... ON CONFLICT {@link CloudDatumStreamSettingsEntity} + * entities. + * + * @author matt + * @version 1.0 + */ +public class UpsertCloudDatumStreamSettingsEntity implements PreparedStatementCreator, SqlProvider { + + private static final String SQL = """ + INSERT INTO solardin.cin_datum_stream_settings ( + user_id,ds_id,created,modified,pub_in,pub_flux + ) + VALUES (?,?,?,?,?,?) + ON CONFLICT (user_id, ds_id) DO UPDATE + SET modified = COALESCE(EXCLUDED.modified, CURRENT_TIMESTAMP) + , pub_in = EXCLUDED.pub_in + , pub_flux = EXCLUDED.pub_flux + """; + + private final Long userId; + private final Long datumStreamId; + private final CloudDatumStreamSettingsEntity entity; + + /** + * Constructor. + * + * @param userId + * the user ID + * @param datumStreamId + * the datum stream ID + * @param entity + * the entity + * @throws IllegalArgumentException + * if any argument is {@literal null} + */ + public UpsertCloudDatumStreamSettingsEntity(Long userId, Long datumStreamId, + CloudDatumStreamSettingsEntity entity) { + super(); + this.userId = requireNonNullArgument(userId, "userId"); + this.datumStreamId = requireNonNullArgument(datumStreamId, "datumStreamId"); + this.entity = requireNonNullArgument(entity, "entity"); + } + + @Override + public String getSql() { + return SQL; + } + + @Override + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + Timestamp ts = Timestamp.from(entity.getCreated() != null ? entity.getCreated() : Instant.now()); + Timestamp mod = entity.getModified() != null ? Timestamp.from(entity.getModified()) : ts; + PreparedStatement stmt = con.prepareStatement(getSql(), Statement.NO_GENERATED_KEYS); + int p = 0; + stmt.setObject(++p, userId); + stmt.setObject(++p, datumStreamId); + stmt.setTimestamp(++p, ts); + stmt.setTimestamp(++p, mod); + stmt.setBoolean(++p, entity.isPublishToSolarIn()); + stmt.setBoolean(++p, entity.isPublishToSolarFlux()); + return stmt; + } + +} diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/UpsertUserSettingsEntity.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/UpsertUserSettingsEntity.java new file mode 100644 index 000000000..563a1c02c --- /dev/null +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/UpsertUserSettingsEntity.java @@ -0,0 +1,93 @@ +/* ================================================================== + * UpsertUserSettingsEntity.java - 28/10/2024 7:38:26 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.dao.jdbc.sql; + +import static net.solarnetwork.util.ObjectUtils.requireNonNullArgument; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.Instant; +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.SqlProvider; +import net.solarnetwork.central.c2c.domain.UserSettingsEntity; + +/** + * Support for INSERT ... ON CONFLICT {@link UserSettingsEntity} entities. + * + * @author matt + * @version 1.0 + */ +public class UpsertUserSettingsEntity implements PreparedStatementCreator, SqlProvider { + + private static final String SQL = """ + INSERT INTO solardin.cin_user_settings ( + user_id,created,modified,pub_in,pub_flux + ) + VALUES (?,?,?,?,?) + ON CONFLICT (user_id) DO UPDATE + SET modified = COALESCE(EXCLUDED.modified, CURRENT_TIMESTAMP) + , pub_in = EXCLUDED.pub_in + , pub_flux = EXCLUDED.pub_flux + """; + + private final Long userId; + private final UserSettingsEntity entity; + + /** + * Constructor. + * + * @param userId + * the user ID + * @param entity + * the entity + * @throws IllegalArgumentException + * if any argument is {@literal null} + */ + public UpsertUserSettingsEntity(Long userId, UserSettingsEntity entity) { + super(); + this.userId = requireNonNullArgument(userId, "userId"); + this.entity = requireNonNullArgument(entity, "entity"); + } + + @Override + public String getSql() { + return SQL; + } + + @Override + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + Timestamp ts = Timestamp.from(entity.getCreated() != null ? entity.getCreated() : Instant.now()); + Timestamp mod = entity.getModified() != null ? Timestamp.from(entity.getModified()) : ts; + PreparedStatement stmt = con.prepareStatement(getSql(), Statement.NO_GENERATED_KEYS); + int p = 0; + stmt.setObject(++p, userId); + stmt.setTimestamp(++p, ts); + stmt.setTimestamp(++p, mod); + stmt.setBoolean(++p, entity.isPublishToSolarIn()); + stmt.setBoolean(++p, entity.isPublishToSolarFlux()); + return stmt; + } + +} diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/BasicCloudDatumStreamSettings.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/BasicCloudDatumStreamSettings.java new file mode 100644 index 000000000..322fa61ec --- /dev/null +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/BasicCloudDatumStreamSettings.java @@ -0,0 +1,48 @@ +/* ================================================================== + * BasicCloudDatumStreamSettings.java - 28/10/2024 2:26:59 pm + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.domain; + +/** + * Basic implementation of {@link CloudDatumStreamSettings}. + * + * @param publishToSolarIn + * the SolarIn publish mode + * @param publishToSolarFlux + * the SolarFlux publish mode + * @author matt + * @version 1.0 + */ +public record BasicCloudDatumStreamSettings(boolean publishToSolarIn, boolean publishToSolarFlux) + implements CloudDatumStreamSettings { + + @Override + public boolean isPublishToSolarIn() { + return publishToSolarIn; + } + + @Override + public boolean isPublishToSolarFlux() { + return publishToSolarFlux; + } + +} diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/CloudDatumStreamSettings.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/CloudDatumStreamSettings.java new file mode 100644 index 000000000..521f8503a --- /dev/null +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/CloudDatumStreamSettings.java @@ -0,0 +1,49 @@ +/* ================================================================== + * CloudDatumStreamSettings.java - 28/10/2024 7:28:37 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.domain; + +/** + * Cloud datum stream configurable settings. + * + * @author matt + * @version 1.0 + */ +public interface CloudDatumStreamSettings { + + /** + * Get the "publish to SolarIn" toggle. + * + * @return {@literal true} if data should be published to SolarIn; defaults + * to {@literal true} + */ + boolean isPublishToSolarIn(); + + /** + * Get the "publish to SolarFlux" toggle. + * + * @return {@literal true} if data should be published to SolarFlux; + * defaults to {@literal true} + */ + boolean isPublishToSolarFlux(); + +} diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/CloudDatumStreamSettingsEntity.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/CloudDatumStreamSettingsEntity.java new file mode 100644 index 000000000..03684c43e --- /dev/null +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/CloudDatumStreamSettingsEntity.java @@ -0,0 +1,169 @@ +/* ================================================================== + * CloudDatumStreamSettingsEntity.java - 28/10/2024 7:15:32 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.domain; + +import java.time.Instant; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import net.solarnetwork.central.dao.BaseUserModifiableEntity; +import net.solarnetwork.central.domain.UserLongCompositePK; + +/** + * Cloud datum stream settings, to override {@link UserSettingsEntity}. + * + * @author matt + * @version 1.0 + */ +@JsonIgnoreProperties({ "id", "enabled" }) +@JsonPropertyOrder({ "userId", "datumStreamId", "created", "modified", "publishToSolarIn", + "publishToSolarFlux" }) +public class CloudDatumStreamSettingsEntity + extends BaseUserModifiableEntity implements + CloudIntegrationsConfigurationEntity, + CloudDatumStreamSettings { + + private static final long serialVersionUID = -5768166630955664067L; + + private boolean publishToSolarIn = true; + private boolean publishToSolarFlux = false; + + /** + * Constructor. + * + * @param userId + * the user ID + * @param dataSourceId + * the data source ID + * @param created + * the creation date + * @throws IllegalArgumentException + * if any argument is {@literal null} + */ + public CloudDatumStreamSettingsEntity(UserLongCompositePK id, Instant created) { + super(id, created); + } + + /** + * Constructor. + * + * @param userId + * the user ID + * @param dataSourceId + * the data source ID + * @param created + * the creation date + * @throws IllegalArgumentException + * if any argument is {@literal null} + */ + public CloudDatumStreamSettingsEntity(Long userId, Long dataSourceId, Instant created) { + this(new UserLongCompositePK(userId, dataSourceId), created); + } + + @Override + public boolean isFullyConfigured() { + return true; + } + + @Override + public CloudDatumStreamSettingsEntity copyWithId(UserLongCompositePK id) { + var copy = new CloudDatumStreamSettingsEntity(id, getCreated()); + copyTo(copy); + return copy; + } + + @Override + public void copyTo(CloudDatumStreamSettingsEntity entity) { + super.copyTo(entity); + entity.setPublishToSolarIn(publishToSolarIn); + entity.setPublishToSolarFlux(publishToSolarFlux); + } + + @Override + public boolean isSameAs(CloudDatumStreamSettingsEntity other) { + boolean result = super.isSameAs(other); + if ( !result ) { + return false; + } + // @formatter:off + return publishToSolarIn == other.publishToSolarIn + && publishToSolarFlux == other.publishToSolarFlux + ; + // @formatter:on + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(64); + builder.append("CloudDatumStreamSettingsEntity{userId="); + builder.append(getUserId()); + builder.append(", datumStreamId="); + builder.append(getDatumStreamId()); + builder.append(", publishToSolarIn="); + builder.append(publishToSolarIn); + builder.append(", publishToSolarFlux="); + builder.append(publishToSolarFlux); + builder.append("}"); + return builder.toString(); + } + + /** + * Get the cloud datum stream ID. + * + * @return the cloud datum stream ID + */ + public Long getDatumStreamId() { + UserLongCompositePK id = getId(); + return (id != null ? id.getEntityId() : null); + } + + @Override + public boolean isPublishToSolarIn() { + return publishToSolarIn; + } + + /** + * Set the "publish to SolarIn" toggle. + * + * @param publishToSolarIn + * {@literal true} if data should be published to SolarIn + */ + public void setPublishToSolarIn(boolean publishToSolarIn) { + this.publishToSolarIn = publishToSolarIn; + } + + @Override + public boolean isPublishToSolarFlux() { + return publishToSolarFlux; + } + + /** + * Set the "publish to SolarFlux" toggle. + * + * @param publishToSolarFlux + * {@literal true} if data should be published to SolarFlux + */ + public void setPublishToSolarFlux(boolean publishToSolarFlux) { + this.publishToSolarFlux = publishToSolarFlux; + } + +} diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/UserSettingsEntity.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/UserSettingsEntity.java new file mode 100644 index 000000000..709bc02d1 --- /dev/null +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/UserSettingsEntity.java @@ -0,0 +1,196 @@ +/* ================================================================== + * UserSettingsEntity.java - 28/10/2024 6:59:38 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.domain; + +import static net.solarnetwork.util.ObjectUtils.requireNonNullArgument; +import java.io.Serializable; +import java.time.Instant; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import net.solarnetwork.central.dao.UserRelatedEntity; +import net.solarnetwork.dao.BasicLongEntity; +import net.solarnetwork.domain.CopyingIdentity; +import net.solarnetwork.domain.Differentiable; + +/** + * Account-wide cloud integrations settings. + * + *

+ * The {@link #getId()} value represents the SolarNet user ID. + *

+ * + * @author matt + * @version 1.0 + */ +@JsonIgnoreProperties({ "id" }) +@JsonPropertyOrder({ "userId", "created", "modified", "publishToSolarIn", "publishToSolarFlux" }) +public class UserSettingsEntity extends BasicLongEntity + implements Differentiable, UserRelatedEntity, + CopyingIdentity, Serializable, Cloneable, CloudDatumStreamSettings { + + private static final long serialVersionUID = 2463852724878062639L; + + private Instant modified; + private boolean publishToSolarIn = true; + private boolean publishToSolarFlux = false; + + /** + * Constructor. + * + * @param userId + * the user ID + * @param created + * the creation date + * @throws IllegalArgumentException + * if any argument is {@literal null} + */ + public UserSettingsEntity(Long userId, Instant created) { + super(userId, created); + } + + @Override + public UserSettingsEntity copyWithId(Long id) { + var copy = new UserSettingsEntity(requireNonNullArgument(id, "id"), getCreated()); + copyTo(copy); + return copy; + } + + @Override + public void copyTo(UserSettingsEntity entity) { + entity.setModified(modified); + entity.setPublishToSolarIn(publishToSolarIn); + entity.setPublishToSolarFlux(publishToSolarFlux); + } + + @Override + public boolean differsFrom(UserSettingsEntity other) { + return !isSameAs(other); + } + + /** + * Test if the properties of another entity are the same as in this + * instance. + * + *

+ * The {@code id} and {@code created} properties are not compared by this + * method. + *

+ * + * @param other + * the other entity to compare to + * @return {@literal true} if the properties of this instance are equal to + * the other + */ + public boolean isSameAs(UserSettingsEntity other) { + if ( other == null ) { + return false; + } + // @formatter:off + return publishToSolarIn == other.publishToSolarIn + && publishToSolarFlux == other.publishToSolarFlux + ; + // @formatter:on + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("CloudDatumStream{userId="); + builder.append(getUserId()); + builder.append(", publishToSolarIn="); + builder.append(publishToSolarIn); + builder.append(", publishToSolarFlux="); + builder.append(publishToSolarFlux); + builder.append("}"); + return builder.toString(); + } + + @Override + public UserSettingsEntity clone() { + return (UserSettingsEntity) super.clone(); + } + + /** + * Get the user ID. + * + *

+ * This is an alias for {@link #getId()}. + *

+ * + * @return the user ID + */ + @Override + public Long getUserId() { + return getId(); + } + + /** + * Get the modification date. + * + * @return the modified date + */ + public Instant getModified() { + return modified; + } + + /** + * Set the modification date. + * + * @param modified + * the modified date to set + */ + public void setModified(Instant modified) { + this.modified = modified; + } + + @Override + public boolean isPublishToSolarIn() { + return publishToSolarIn; + } + + /** + * Set the "publish to SolarIn" toggle. + * + * @param publishToSolarIn + * {@literal true} if data should be published to SolarIn + */ + public void setPublishToSolarIn(boolean publishToSolarIn) { + this.publishToSolarIn = publishToSolarIn; + } + + @Override + public boolean isPublishToSolarFlux() { + return publishToSolarFlux; + } + + /** + * Set the "publish to SolarFlux" toggle. + * + * @param publishToSolarFlux + * {@literal true} if data should be published to SolarFlux + */ + public void setPublishToSolarFlux(boolean publishToSolarFlux) { + this.publishToSolarFlux = publishToSolarFlux; + } + +} diff --git a/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/biz/impl/test/DaoCloudDatumStreamPollServiceTests.java b/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/biz/impl/test/DaoCloudDatumStreamPollServiceTests.java index 345a1a6fc..1e932d806 100644 --- a/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/biz/impl/test/DaoCloudDatumStreamPollServiceTests.java +++ b/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/biz/impl/test/DaoCloudDatumStreamPollServiceTests.java @@ -24,6 +24,7 @@ import static java.time.Instant.now; import static java.time.ZoneOffset.UTC; +import static net.solarnetwork.central.c2c.biz.impl.DaoCloudDatumStreamPollService.DEFAULT_DATUM_STREAM_SETTINGS; import static net.solarnetwork.central.domain.BasicClaimableJobState.Claimed; import static net.solarnetwork.central.domain.BasicClaimableJobState.Completed; import static net.solarnetwork.central.domain.BasicClaimableJobState.Executing; @@ -65,6 +66,7 @@ import net.solarnetwork.central.c2c.biz.impl.DaoCloudDatumStreamPollService; import net.solarnetwork.central.c2c.dao.CloudDatumStreamConfigurationDao; import net.solarnetwork.central.c2c.dao.CloudDatumStreamPollTaskDao; +import net.solarnetwork.central.c2c.dao.CloudDatumStreamSettingsEntityDao; import net.solarnetwork.central.c2c.domain.BasicCloudDatumStreamQueryResult; import net.solarnetwork.central.c2c.domain.CloudDatumStreamConfiguration; import net.solarnetwork.central.c2c.domain.CloudDatumStreamPollTaskEntity; @@ -83,7 +85,7 @@ * Test cases for the {@link DaoCloudDatumStreamPollService} class. * * @author matt - * @version 1.1 + * @version 1.2 */ @SuppressWarnings("static-access") @ExtendWith(MockitoExtension.class) @@ -106,6 +108,9 @@ public class DaoCloudDatumStreamPollServiceTests { @Mock private CloudDatumStreamConfigurationDao datumStreamDao; + @Mock + private CloudDatumStreamSettingsEntityDao datumStreamSettingsDao; + @Mock private DatumWriteOnlyDao datumDao; @@ -134,7 +139,8 @@ public void setup() { var datumStreamServices = Map.of(TEST_DATUM_STREAM_SERVICE_IDENTIFIER, datumStreamService); service = new DaoCloudDatumStreamPollService(clock, userEventAppenderBiz, nodeOwnershipDao, - taskDao, datumStreamDao, datumDao, executor, datumStreamServices::get); + taskDao, datumStreamDao, datumStreamSettingsDao, datumDao, executor, + datumStreamServices::get); } @Test @@ -201,6 +207,10 @@ public void executeTask() throws Exception { // look up datum stream associated with task given(datumStreamDao.get(datumStream.getId())).willReturn(datumStream); + // resolve datum stream settings + given(datumStreamSettingsDao.resolveSettings(TEST_USER_ID, datumStream.getConfigId(), + DEFAULT_DATUM_STREAM_SETTINGS)).willReturn(DEFAULT_DATUM_STREAM_SETTINGS); + // verify node ownership final var nodeOwner = new BasicSolarNodeOwnership(datumStream.getObjectId(), TEST_USER_ID, "NZ", UTC, true, false); @@ -301,6 +311,10 @@ public void executeTask_notNodeOwner() throws Exception { // look up datum stream associated with task given(datumStreamDao.get(datumStream.getId())).willReturn(datumStream); + // resolve datum stream settings + given(datumStreamSettingsDao.resolveSettings(TEST_USER_ID, datumStream.getConfigId(), + DEFAULT_DATUM_STREAM_SETTINGS)).willReturn(DEFAULT_DATUM_STREAM_SETTINGS); + // verify node ownership (returning different user, so not owner) final var nodeOwner = new BasicSolarNodeOwnership(datumStream.getObjectId(), -1L, "NZ", UTC, true, false); diff --git a/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/dao/jdbc/test/CinJdbcTestUtils.java b/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/dao/jdbc/test/CinJdbcTestUtils.java index e7a762a85..b223e9f49 100644 --- a/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/dao/jdbc/test/CinJdbcTestUtils.java +++ b/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/dao/jdbc/test/CinJdbcTestUtils.java @@ -35,8 +35,10 @@ import net.solarnetwork.central.c2c.domain.CloudDatumStreamMappingConfiguration; import net.solarnetwork.central.c2c.domain.CloudDatumStreamPollTaskEntity; import net.solarnetwork.central.c2c.domain.CloudDatumStreamPropertyConfiguration; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettingsEntity; import net.solarnetwork.central.c2c.domain.CloudDatumStreamValueType; import net.solarnetwork.central.c2c.domain.CloudIntegrationConfiguration; +import net.solarnetwork.central.c2c.domain.UserSettingsEntity; import net.solarnetwork.central.domain.BasicClaimableJobState; import net.solarnetwork.domain.datum.DatumSamplesType; import net.solarnetwork.domain.datum.ObjectDatumKind; @@ -45,7 +47,7 @@ * Helper methods for cloud integrations JDBC tests. * * @author matt - * @version 1.1 + * @version 1.2 */ public class CinJdbcTestUtils { @@ -283,7 +285,7 @@ public static CloudDatumStreamPollTaskEntity newCloudDatumStreamPollTaskEntity(L } /** - * List datum stream configuration rows. + * List datum stream poll task rows. * * @param jdbcOps * the JDBC operations @@ -298,4 +300,80 @@ public static List> allCloudDatumStreamPollTaskEntityData( return data; } + /** + * Create a new user settings instance. + * + * @param userId + * the user ID + * @param publishToSolarIn + * the SolarIn publish mode + * @param publishToSolarFlux + * the SolarFlux publish mode + * @return the entity + * @since 1.2 + */ + public static UserSettingsEntity newUserSettingsEntity(Long userId, boolean publishToSolarIn, + boolean publishToSolarFlux) { + UserSettingsEntity conf = new UserSettingsEntity(userId, Instant.now()); + conf.setPublishToSolarIn(publishToSolarIn); + conf.setPublishToSolarFlux(publishToSolarFlux); + return conf; + } + + /** + * List user settings rows. + * + * @param jdbcOps + * the JDBC operations + * @return the rows + * @since 1.2 + */ + public static List> allUserSettingsEntityData(JdbcOperations jdbcOps) { + List> data = jdbcOps + .queryForList("select * from solardin.cin_user_settings ORDER BY user_id"); + log.debug("solardin.cin_user_settings table has {} items: [{}]", data.size(), + data.stream().map(Object::toString).collect(joining("\n\t", "\n\t", "\n"))); + return data; + } + + /** + * Create a new datum stream settings instance. + * + * @param userId + * the user ID + * @param datumStreamId + * the datum stream ID + * @param publishToSolarIn + * the SolarIn publish mode + * @param publishToSolarFlux + * the SolarFlux publish mode + * @return the entity + * @since 1.2 + */ + public static CloudDatumStreamSettingsEntity newCloudDatumStreamSettingsEntity(Long userId, + Long datumStreamId, boolean publishToSolarIn, boolean publishToSolarFlux) { + CloudDatumStreamSettingsEntity conf = new CloudDatumStreamSettingsEntity(userId, datumStreamId, + Instant.now()); + conf.setPublishToSolarIn(publishToSolarIn); + conf.setPublishToSolarFlux(publishToSolarFlux); + return conf; + } + + /** + * List datum stream settings rows. + * + * @param jdbcOps + * the JDBC operations + * @return the rows + * @since 1.2 + */ + public static List> allCloudDatumStreamSettingsEntityData( + JdbcOperations jdbcOps) { + List> data = jdbcOps + .queryForList("select * from solardin.cin_datum_stream_settings ORDER BY user_id"); + log.debug("solardin.cin_datum_stream_settings table has {} items: [{}]", data.size(), + data.stream().map(Object::toString).collect(joining("\n\t", "\n\t", "\n"))); + return data; + } + } diff --git a/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/dao/jdbc/test/JdbcCloudDatumStreamSettingsEntityDaoTests.java b/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/dao/jdbc/test/JdbcCloudDatumStreamSettingsEntityDaoTests.java new file mode 100644 index 000000000..24e754c0d --- /dev/null +++ b/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/dao/jdbc/test/JdbcCloudDatumStreamSettingsEntityDaoTests.java @@ -0,0 +1,340 @@ +/* ================================================================== + * JdbcCloudDatumStreamSettingsEntityDaoTests.java - 28/10/2024 10:40:14 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.dao.jdbc.test; + +import static net.solarnetwork.central.c2c.dao.jdbc.test.CinJdbcTestUtils.allCloudDatumStreamSettingsEntityData; +import static net.solarnetwork.central.c2c.dao.jdbc.test.CinJdbcTestUtils.newCloudDatumStreamSettingsEntity; +import static net.solarnetwork.central.test.CommonTestUtils.randomBoolean; +import static net.solarnetwork.central.test.CommonTestUtils.randomLong; +import static net.solarnetwork.central.test.CommonTestUtils.randomString; +import static org.assertj.core.api.BDDAssertions.from; +import static org.assertj.core.api.BDDAssertions.then; +import static org.assertj.core.api.InstanceOfAssertFactories.list; +import static org.assertj.core.api.InstanceOfAssertFactories.map; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import net.solarnetwork.central.c2c.dao.jdbc.JdbcCloudDatumStreamConfigurationDao; +import net.solarnetwork.central.c2c.dao.jdbc.JdbcCloudDatumStreamMappingConfigurationDao; +import net.solarnetwork.central.c2c.dao.jdbc.JdbcCloudDatumStreamSettingsEntityDao; +import net.solarnetwork.central.c2c.dao.jdbc.JdbcCloudIntegrationConfigurationDao; +import net.solarnetwork.central.c2c.dao.jdbc.JdbcUserSettingsEntityDao; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamConfiguration; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamMappingConfiguration; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettings; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettingsEntity; +import net.solarnetwork.central.c2c.domain.CloudIntegrationConfiguration; +import net.solarnetwork.central.c2c.domain.UserSettingsEntity; +import net.solarnetwork.central.domain.UserLongCompositePK; +import net.solarnetwork.central.test.AbstractJUnit5JdbcDaoTestSupport; +import net.solarnetwork.central.test.CommonDbTestUtils; +import net.solarnetwork.central.test.CommonTestUtils; +import net.solarnetwork.dao.Entity; +import net.solarnetwork.domain.datum.ObjectDatumKind; + +/** + * Test cases for the {@link JdbcCloudDatumStreamSettingsEntityDao} class. + * + * @author matt + * @version 1.0 + */ +public class JdbcCloudDatumStreamSettingsEntityDaoTests extends AbstractJUnit5JdbcDaoTestSupport { + + private JdbcCloudIntegrationConfigurationDao integrationDao; + private JdbcCloudDatumStreamMappingConfigurationDao datumStreamMappingDao; + private JdbcCloudDatumStreamConfigurationDao datumStreamDao; + private JdbcUserSettingsEntityDao userSettingsDao; + private JdbcCloudDatumStreamSettingsEntityDao dao; + private Long userId; + + private CloudDatumStreamSettingsEntity last; + + @BeforeEach + public void setup() { + dao = new JdbcCloudDatumStreamSettingsEntityDao(jdbcTemplate); + userId = CommonDbTestUtils.insertUser(jdbcTemplate); + integrationDao = new JdbcCloudIntegrationConfigurationDao(jdbcTemplate); + datumStreamMappingDao = new JdbcCloudDatumStreamMappingConfigurationDao(jdbcTemplate); + datumStreamDao = new JdbcCloudDatumStreamConfigurationDao(jdbcTemplate); + userSettingsDao = new JdbcUserSettingsEntityDao(jdbcTemplate); + } + + private CloudIntegrationConfiguration createIntegration(Long userId) { + CloudIntegrationConfiguration conf = CinJdbcTestUtils.newCloudIntegrationConfiguration(userId, + randomString(), randomString(), null); + CloudIntegrationConfiguration entity = integrationDao.get(integrationDao.save(conf)); + return entity; + } + + private CloudDatumStreamMappingConfiguration createDatumStreamMapping(Long userId, + Long integrationId) { + CloudDatumStreamMappingConfiguration conf = CinJdbcTestUtils + .newCloudDatumStreamMappingConfiguration(userId, integrationId, randomString(), null); + CloudDatumStreamMappingConfiguration entity = datumStreamMappingDao + .get(datumStreamMappingDao.save(conf)); + return entity; + } + + private CloudDatumStreamConfiguration createDatumStream(Long userId, Long datumStreamMappingId) { + CloudDatumStreamConfiguration conf = CinJdbcTestUtils.newCloudDatumStreamConfiguration(userId, + datumStreamMappingId, randomString(), ObjectDatumKind.Node, randomLong(), randomString(), + randomString(), randomString(), null); + conf.setEnabled(true); + CloudDatumStreamConfiguration entity = datumStreamDao.get(datumStreamDao.save(conf)); + return entity; + } + + private UserSettingsEntity createUserSettings(Long userId) { + UserSettingsEntity conf = CinJdbcTestUtils.newUserSettingsEntity(userId, randomBoolean(), + randomBoolean()); + UserSettingsEntity entity = userSettingsDao.get(userSettingsDao.store(conf)); + return entity; + } + + @Test + public void entityKey() { + UserLongCompositePK id = new UserLongCompositePK(randomLong(), randomLong()); + CloudDatumStreamSettingsEntity result = dao.entityKey(id); + + // @formatter:off + then(result) + .as("Entity for key returned") + .isNotNull() + .as("ID of entity from provided value") + .returns(id, Entity::getId) + ; + // @formatter:on + } + + @Test + public void insert() { + // GIVEN + final CloudIntegrationConfiguration integration = createIntegration(userId); + final CloudDatumStreamMappingConfiguration mapping = createDatumStreamMapping(userId, + integration.getConfigId()); + final CloudDatumStreamConfiguration datumStream = createDatumStream(userId, + mapping.getConfigId()); + + // @formatter:off + CloudDatumStreamSettingsEntity conf = newCloudDatumStreamSettingsEntity(userId, + datumStream.getConfigId(), + randomBoolean(), + randomBoolean() + ); + // @formatter:on + + // WHEN + UserLongCompositePK result = dao.create(userId, conf); + + // THEN + + // @formatter:off + then(result).as("Primary key") + .isNotNull() + .as("User ID as provided") + .returns(userId, UserLongCompositePK::getUserId) + .as("ID generated") + .doesNotReturn(null, UserLongCompositePK::getEntityId) + ; + + List> data = allCloudDatumStreamSettingsEntityData(jdbcTemplate); + then(data).as("Table has 1 row").hasSize(1).asInstanceOf(list(Map.class)) + .element(0, map(String.class, Object.class)) + .as("Row user ID") + .containsEntry("user_id", userId) + .as("Row datum stream ID") + .containsEntry("ds_id", datumStream.getConfigId()) + .as("Row creation date") + .containsEntry("created", Timestamp.from(conf.getCreated())) + .as("Row modification date (populated with creation date)") + .containsEntry("modified", Timestamp.from(conf.getCreated())) + .as("Row publish SolarIn") + .containsEntry("pub_in", conf.isPublishToSolarIn()) + .as("Row publish SolarFlux") + .containsEntry("pub_flux", conf.isPublishToSolarFlux()) + ; + // @formatter:on + last = conf.copyWithId(result); + } + + @Test + public void get() { + // GIVEN + insert(); + + // WHEN + CloudDatumStreamSettingsEntity result = dao.get(last.getId()); + + // THEN + then(result).as("Retrieved entity matches source").isEqualTo(last); + } + + @Test + public void update() { + // GIVEN + insert(); + + // WHEN + CloudDatumStreamSettingsEntity conf = last.copyWithId(last.getId()); + conf.setModified(Instant.now().plusMillis(474)); + conf.setPublishToSolarIn(!conf.isPublishToSolarIn()); + conf.setPublishToSolarFlux(!conf.isPublishToSolarFlux()); + + UserLongCompositePK result = dao.save(conf); + CloudDatumStreamSettingsEntity updated = dao.get(result); + + // THEN + List> data = allCloudDatumStreamSettingsEntityData(jdbcTemplate); + then(data).as("Table has 1 row").hasSize(1); + // @formatter:off + then(updated).as("Retrieved entity matches updated source") + .isEqualTo(conf) + .as("Entity saved updated values") + .matches(c -> c.isSameAs(updated)); + // @formatter:on + } + + @Test + public void delete() { + // GIVEN + insert(); + + // WHEN + dao.delete(last); + + // THEN + List> data = allCloudDatumStreamSettingsEntityData(jdbcTemplate); + then(data).as("Row deleted from db").isEmpty(); + } + + @Test + public void findForUser() throws Exception { + // GIVEN + final int userCount = 3; + final int count = 3; + final List confs = new ArrayList<>(count); + + for ( int u = 0; u < userCount; u++ ) { + Long userId = CommonDbTestUtils.insertUser(jdbcTemplate); + Long integrationId = createIntegration(userId).getConfigId(); + Long mappingId = createDatumStreamMapping(userId, integrationId).getConfigId(); + for ( int i = 0; i < count; i++ ) { + Long datumStreamId = createDatumStream(userId, mappingId).getConfigId(); + // @formatter:off + CloudDatumStreamSettingsEntity conf = newCloudDatumStreamSettingsEntity(userId, + datumStreamId, + randomBoolean(), + randomBoolean() + ); + // @formatter:on + UserLongCompositePK id = dao.create(userId, conf); + conf = conf.copyWithId(id); + confs.add(conf); + } + } + + // WHEN + final CloudDatumStreamSettingsEntity randomEntity = confs + .get(CommonTestUtils.RNG.nextInt(confs.size())); + Collection results = dao.findAll(randomEntity.getUserId(), null); + + // THEN + CloudDatumStreamSettingsEntity[] expected = confs.stream() + .filter(e -> randomEntity.getUserId().equals(e.getUserId())) + .toArray(CloudDatumStreamSettingsEntity[]::new); + then(results).as("Results for single user returned").containsExactly(expected); + } + + @Test + public void resolve_none() { + // GIVEN + UserSettingsEntity defaultSettings = new UserSettingsEntity(userId, Instant.now()); + + // WHEN + CloudDatumStreamSettings result = dao.resolveSettings(userId, randomLong(), defaultSettings); + + // THEN + // @formatter:off + then(result) + .as("Default settings instance returned when no rows in database") + .isSameAs(defaultSettings) + ; + // @formatter:on + } + + @Test + public void resolve_user() { + // GIVEN + UserSettingsEntity defaultSettings = new UserSettingsEntity(userId, Instant.now()); + UserSettingsEntity userSettings = createUserSettings(userId); + + // WHEN + CloudDatumStreamSettings result = dao.resolveSettings(userId, randomLong(), defaultSettings); + + // THEN + // @formatter:off + then(result) + .as("Default settings instance NOT returned when user settings row exists in database") + .isNotSameAs(defaultSettings) + .as("Pub SolarIn resolved from user settings when no datum stream settings available") + .returns(userSettings.isPublishToSolarIn(), from(CloudDatumStreamSettings::isPublishToSolarIn)) + .as("Pub SolarFlux resolved from user settings when no datum stream settings available") + .returns(userSettings.isPublishToSolarFlux(), from(CloudDatumStreamSettings::isPublishToSolarFlux)) + .asInstanceOf(InstanceOfAssertFactories.type(CloudDatumStreamSettingsEntity.class)) + .as("Unassigned datum stream ID returned for user-level settings") + .returns(UserLongCompositePK.UNASSIGNED_ENTITY_ID, from(CloudDatumStreamSettingsEntity::getDatumStreamId)) + ; + // @formatter:on + } + + @Test + public void resolve_datumStream() { + // GIVEN + UserSettingsEntity defaultSettings = new UserSettingsEntity(userId, Instant.now()); + createUserSettings(userId); + insert(); + + // WHEN + CloudDatumStreamSettings result = dao.resolveSettings(userId, last.getDatumStreamId(), + defaultSettings); + + // THEN + // @formatter:off + then(result) + .as("Default settings instance NOT returned when user settings row exists in database") + .isNotSameAs(defaultSettings) + .as("Pub SolarIn resolved from datum stream settings when no datum stream settings available") + .returns(last.isPublishToSolarIn(), from(CloudDatumStreamSettings::isPublishToSolarIn)) + .as("Pub SolarFlux resolved from datum stream settings when no datum stream settings available") + .returns(last.isPublishToSolarFlux(), from(CloudDatumStreamSettings::isPublishToSolarFlux)) + ; + // @formatter:on + } + +} diff --git a/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/dao/jdbc/test/JdbcUserSettingsEntityDaoTests.java b/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/dao/jdbc/test/JdbcUserSettingsEntityDaoTests.java new file mode 100644 index 000000000..b2fd060a7 --- /dev/null +++ b/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/dao/jdbc/test/JdbcUserSettingsEntityDaoTests.java @@ -0,0 +1,145 @@ +/* ================================================================== + * JdbcUserSettingsEntityDaoTests.java - 28/10/2024 10:23:41 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.c2c.dao.jdbc.test; + +import static net.solarnetwork.central.c2c.dao.jdbc.test.CinJdbcTestUtils.allUserSettingsEntityData; +import static net.solarnetwork.central.c2c.dao.jdbc.test.CinJdbcTestUtils.newUserSettingsEntity; +import static net.solarnetwork.central.test.CommonTestUtils.randomBoolean; +import static org.assertj.core.api.BDDAssertions.then; +import static org.assertj.core.api.InstanceOfAssertFactories.list; +import static org.assertj.core.api.InstanceOfAssertFactories.map; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import net.solarnetwork.central.c2c.dao.jdbc.JdbcUserSettingsEntityDao; +import net.solarnetwork.central.c2c.domain.UserSettingsEntity; +import net.solarnetwork.central.test.AbstractJUnit5JdbcDaoTestSupport; +import net.solarnetwork.central.test.CommonDbTestUtils; + +/** + * Test cases for the {@link JdbcUserSettingsEntityDao} class. + * + * @author matt + * @version 1.0 + */ +public class JdbcUserSettingsEntityDaoTests extends AbstractJUnit5JdbcDaoTestSupport { + + private JdbcUserSettingsEntityDao dao; + private Long userId; + + private UserSettingsEntity last; + + @BeforeEach + public void setup() { + dao = new JdbcUserSettingsEntityDao(jdbcTemplate); + userId = CommonDbTestUtils.insertUser(jdbcTemplate); + } + + @Test + public void insert() { + // GIVEN + UserSettingsEntity conf = newUserSettingsEntity(userId, randomBoolean(), randomBoolean()); + + // WHEN + Long result = dao.store(conf); + + // THEN + + // @formatter:off + then(result).as("Primary key") + .as("User ID as provided") + .isEqualTo(userId) + ; + + List> data = allUserSettingsEntityData(jdbcTemplate); + then(data).as("Table has 1 row").hasSize(1).asInstanceOf(list(Map.class)) + .element(0, map(String.class, Object.class)) + .as("Row user ID") + .containsEntry("user_id", userId) + .as("Row creation date") + .containsEntry("created", Timestamp.from(conf.getCreated())) + .as("Row modification date") + .containsEntry("modified", Timestamp.from(conf.getModified())) + .as("Row publish SolarIn") + .containsEntry("pub_in", conf.isPublishToSolarIn()) + .as("Row publish SolarFlux") + .containsEntry("pub_flux", conf.isPublishToSolarFlux()) + ; + // @formatter:on + last = conf; + } + + @Test + public void get() { + // GIVEN + insert(); + + // WHEN + UserSettingsEntity result = dao.get(last.getId()); + + // THEN + then(result).as("Retrieved entity matches source").isEqualTo(last); + } + + @Test + public void update() { + // GIVEN + insert(); + + // WHEN + UserSettingsEntity conf = last.copyWithId(last.getId()); + conf.setModified(Instant.now().plusMillis(474)); + conf.setPublishToSolarIn(!conf.isPublishToSolarIn()); + conf.setPublishToSolarFlux(!conf.isPublishToSolarFlux()); + + Long result = dao.store(conf); + UserSettingsEntity updated = dao.get(result); + + // THEN + List> data = allUserSettingsEntityData(jdbcTemplate); + then(data).as("Table has 1 row").hasSize(1); + // @formatter:off + then(updated).as("Retrieved entity matches updated source") + .isEqualTo(conf) + .as("Entity saved updated values") + .matches(c -> c.isSameAs(updated)); + // @formatter:on + } + + @Test + public void delete() { + // GIVEN + insert(); + + // WHEN + dao.delete(last); + + // THEN + List> data = allUserSettingsEntityData(jdbcTemplate); + then(data).as("Row deleted from db").isEmpty(); + } + +} diff --git a/solarnet/common-test/src/main/java/net/solarnetwork/central/test/CommonTestUtils.java b/solarnet/common-test/src/main/java/net/solarnetwork/central/test/CommonTestUtils.java index 840983319..d519b3870 100644 --- a/solarnet/common-test/src/main/java/net/solarnetwork/central/test/CommonTestUtils.java +++ b/solarnet/common-test/src/main/java/net/solarnetwork/central/test/CommonTestUtils.java @@ -39,7 +39,7 @@ * Common test utilities. * * @author matt - * @version 1.3 + * @version 1.4 */ public final class CommonTestUtils { @@ -121,6 +121,16 @@ public static Long randomLong() { return Math.abs(RNG.nextLong()); } + /** + * Get a random boolean value. + * + * @return the boolean + * @since 1.4 + */ + public static boolean randomBoolean() { + return RNG.nextBoolean(); + } + /** * Compare a decimal array using a maximum scale. * diff --git a/solarnet/solarjobs/src/main/java/net/solarnetwork/central/jobs/config/CloudIntegrationsDatumStreamPollConfig.java b/solarnet/solarjobs/src/main/java/net/solarnetwork/central/jobs/config/CloudIntegrationsDatumStreamPollConfig.java index 04ee59551..307d155a0 100644 --- a/solarnet/solarjobs/src/main/java/net/solarnetwork/central/jobs/config/CloudIntegrationsDatumStreamPollConfig.java +++ b/solarnet/solarjobs/src/main/java/net/solarnetwork/central/jobs/config/CloudIntegrationsDatumStreamPollConfig.java @@ -41,6 +41,7 @@ import net.solarnetwork.central.c2c.config.SolarNetCloudIntegrationsConfiguration; import net.solarnetwork.central.c2c.dao.CloudDatumStreamConfigurationDao; import net.solarnetwork.central.c2c.dao.CloudDatumStreamPollTaskDao; +import net.solarnetwork.central.c2c.dao.CloudDatumStreamSettingsEntityDao; import net.solarnetwork.central.dao.SolarNodeOwnershipDao; import net.solarnetwork.central.datum.v2.dao.DatumWriteOnlyDao; @@ -48,7 +49,7 @@ * Cloud integrations datum stream poll configuration. * * @author matt - * @version 1.0 + * @version 1.1 */ @Profile(CLOUD_INTEGRATIONS) @Configuration(proxyBeanMethods = false) @@ -66,6 +67,9 @@ public class CloudIntegrationsDatumStreamPollConfig implements SolarNetCloudInte @Autowired private CloudDatumStreamConfigurationDao datumStreamDao; + @Autowired + private CloudDatumStreamSettingsEntityDao datumStreamSettingsDao; + @Autowired private DatumWriteOnlyDao datumWriteOnlyDao; @@ -86,7 +90,7 @@ public CloudDatumStreamPollService cloudDatumStreamPollService( var dsMap = datumStreamServices.stream() .collect(Collectors.toMap(CloudDatumStreamService::getId, Function.identity())); var service = new DaoCloudDatumStreamPollService(Clock.systemUTC(), userEventAppenderBiz, - nodeOwnershipDao, taskDao, datumStreamDao, datumWriteOnlyDao, + nodeOwnershipDao, taskDao, datumStreamDao, datumStreamSettingsDao, datumWriteOnlyDao, taskExecutor.getThreadPoolExecutor(), dsMap::get); return service; } diff --git a/solarnet/solaruser/src/main/java/net/solarnetwork/central/reg/web/api/v1/UserCloudIntegrationsController.java b/solarnet/solaruser/src/main/java/net/solarnetwork/central/reg/web/api/v1/UserCloudIntegrationsController.java index 9fe33d61b..3c174413d 100644 --- a/solarnet/solaruser/src/main/java/net/solarnetwork/central/reg/web/api/v1/UserCloudIntegrationsController.java +++ b/solarnet/solaruser/src/main/java/net/solarnetwork/central/reg/web/api/v1/UserCloudIntegrationsController.java @@ -55,7 +55,10 @@ import net.solarnetwork.central.c2c.domain.CloudDatumStreamPollTaskEntity; import net.solarnetwork.central.c2c.domain.CloudDatumStreamPropertyConfiguration; import net.solarnetwork.central.c2c.domain.CloudDatumStreamQueryResult; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettings; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettingsEntity; import net.solarnetwork.central.c2c.domain.CloudIntegrationConfiguration; +import net.solarnetwork.central.c2c.domain.UserSettingsEntity; import net.solarnetwork.central.domain.BasicClaimableJobState; import net.solarnetwork.central.domain.UserLongCompositePK; import net.solarnetwork.central.domain.UserLongIntegerCompositePK; @@ -65,7 +68,9 @@ import net.solarnetwork.central.user.c2c.domain.CloudDatumStreamPollTaskEntityInput; import net.solarnetwork.central.user.c2c.domain.CloudDatumStreamPollTaskStateInput; import net.solarnetwork.central.user.c2c.domain.CloudDatumStreamPropertyConfigurationInput; +import net.solarnetwork.central.user.c2c.domain.CloudDatumStreamSettingsEntityInput; import net.solarnetwork.central.user.c2c.domain.CloudIntegrationConfigurationInput; +import net.solarnetwork.central.user.c2c.domain.UserSettingsEntityInput; import net.solarnetwork.central.web.GlobalExceptionRestController; import net.solarnetwork.dao.FilterResults; import net.solarnetwork.domain.LocalizedServiceInfo; @@ -76,7 +81,7 @@ * Web service API for cloud integrations management. * * @author matt - * @version 1.4 + * @version 1.5 */ @Profile(SolarNetCloudIntegrationsConfiguration.CLOUD_INTEGRATIONS) @GlobalExceptionRestController @@ -158,6 +163,28 @@ public Result> availableCloudDatumStreamFilters( return success(result); } + /*-======================= + * User Settings + *-======================= */ + + @RequestMapping(value = "/settings", method = RequestMethod.GET) + public Result viewUserSettings() { + var result = userCloudIntegrationsBiz.settingsForUser(getCurrentActorUserId()); + return success(result); + } + + @RequestMapping(value = "/settings", method = RequestMethod.PUT) + public Result saveUserSettings( + @Valid @RequestBody UserSettingsEntityInput input) { + return success(userCloudIntegrationsBiz.saveSettings(getCurrentActorUserId(), input)); + } + + @RequestMapping(value = "/settings", method = RequestMethod.DELETE) + public Result deleteUserSettings() { + userCloudIntegrationsBiz.deleteSettings(getCurrentActorUserId()); + return success(); + } + /*-======================= * Integrations *-======================= */ @@ -431,6 +458,47 @@ public Result deleteCloudDatumStreamConfiguration( return success(); } + /*-======================= + * Datum Stream Settings + *-======================= */ + + @RequestMapping(value = "/datum-streams/default-settings", method = RequestMethod.GET) + public Result getDefaultCloudDatumStreamSettings() { + return success(userCloudIntegrationsBiz.defaultDatumStreamSettings()); + } + + @RequestMapping(value = "/datum-streams/{datumStreamId}/settings", method = RequestMethod.GET) + public Result getCloudDatumStreamSettings( + @PathVariable("datumStreamId") Long datumStreamId) { + var id = new UserLongCompositePK(getCurrentActorUserId(), datumStreamId); + return success( + userCloudIntegrationsBiz.configurationForId(id, CloudDatumStreamSettingsEntity.class)); + } + + @RequestMapping(value = "/datum-streams/settings", method = RequestMethod.GET) + public Result> listCloudDatumStreamSettings( + BasicFilter filter) { + var result = userCloudIntegrationsBiz.listConfigurationsForUser(getCurrentActorUserId(), filter, + CloudDatumStreamSettingsEntity.class); + return success(result); + } + + @RequestMapping(value = "/datum-streams/{datumStreamId}/settings", method = RequestMethod.PUT) + public Result saveCloudDatumStreamSettings( + @PathVariable("datumStreamId") Long datumStreamId, + @Valid @RequestBody CloudDatumStreamSettingsEntityInput input) { + var id = new UserLongCompositePK(getCurrentActorUserId(), datumStreamId); + return success(userCloudIntegrationsBiz.saveConfiguration(id, input)); + } + + @RequestMapping(value = "/datum-streams/{datumStreamId}/settings", method = RequestMethod.DELETE) + public Result deleteCloudDatumStreamSettings( + @PathVariable("datumStreamId") Long datumStreamId) { + var id = new UserLongCompositePK(getCurrentActorUserId(), datumStreamId); + userCloudIntegrationsBiz.deleteConfiguration(id, CloudDatumStreamSettingsEntity.class); + return success(); + } + /*-======================= * Datum Stream datum *-======================= */ diff --git a/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/aop/UserCloudIntegrationsSecurityAspect.java b/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/aop/UserCloudIntegrationsSecurityAspect.java index 5c593d266..bdfbb5e3d 100644 --- a/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/aop/UserCloudIntegrationsSecurityAspect.java +++ b/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/aop/UserCloudIntegrationsSecurityAspect.java @@ -61,6 +61,16 @@ public UserCloudIntegrationsSecurityAspect(SolarNodeOwnershipDao nodeOwnershipDa public void readForUserKey(UserIdRelated userKey) { } + /** + * Match read methods given a user ID. + * + * @param userId + * the user ID + */ + @Pointcut("execution(* net.solarnetwork.central.user.c2c.biz.UserCloudIntegrationsBiz.*ForUser(..)) && args(userId,..)") + public void readForUserId(Long userId) { + } + /** * Match list methods given a user-related identifier. * @@ -111,6 +121,16 @@ public void updateEntityForUserKey(UserIdRelated userKey) { public void saveEntityForUserKey(UserIdRelated userKey) { } + /** + * Match save methods given a user ID. + * + * @param userId + * the user ID + */ + @Pointcut("execution(* net.solarnetwork.central.user.c2c.biz.UserCloudIntegrationsBiz.save*(..)) && args(userId,..)") + public void saveEntityForUserId(Long userId) { + } + /** * Match delete methods given an entity. * @@ -121,11 +141,26 @@ public void saveEntityForUserKey(UserIdRelated userKey) { public void deleteEntityForUserKey(UserIdRelated userKey) { } + /** + * Match delete methods given a user ID. + * + * @param userId + * the user ID + */ + @Pointcut("execution(* net.solarnetwork.central.user.c2c.biz.UserCloudIntegrationsBiz.delete*(..)) && args(userId,..)") + public void deleteEntityForUserId(Long userId) { + } + @Before("readForUserKey(userKey)") public void userKeyReadAccessCheck(UserIdRelated userKey) { requireUserReadAccess(userKey != null ? userKey.getUserId() : null); } + @Before("readForUserId(userId)") + public void userIdReadAccessCheck(Long userId) { + requireUserReadAccess(userId); + } + @Before("listForUserId(userId)") public void userIdListAccessCheck(Long userId) { requireUserReadAccess(userId); @@ -146,6 +181,11 @@ public void saveEntityAccessCheck(UserIdRelated userKey) { requireUserWriteAccess(userKey != null ? userKey.getUserId() : null); } + @Before("saveEntityForUserId(userId)") + public void saveEntityForUserAccessCheck(Long userId) { + requireUserWriteAccess(userId); + } + @Before("updateEntityForUserKey(userKey)") public void updateEntityAccessCheck(UserIdRelated userKey) { requireUserWriteAccess(userKey != null ? userKey.getUserId() : null); @@ -156,4 +196,9 @@ public void deleteEntityAccessCheck(UserIdRelated userKey) { requireUserWriteAccess(userKey != null ? userKey.getUserId() : null); } + @Before("deleteEntityForUserId(userId)") + public void userIdDeleteAccessCheck(Long userId) { + requireUserWriteAccess(userId); + } + } diff --git a/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/biz/UserCloudIntegrationsBiz.java b/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/biz/UserCloudIntegrationsBiz.java index 2138b6e03..1ca4c0ddf 100644 --- a/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/biz/UserCloudIntegrationsBiz.java +++ b/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/biz/UserCloudIntegrationsBiz.java @@ -35,14 +35,17 @@ import net.solarnetwork.central.c2c.domain.CloudDatumStreamPropertyConfiguration; import net.solarnetwork.central.c2c.domain.CloudDatumStreamQueryFilter; import net.solarnetwork.central.c2c.domain.CloudDatumStreamQueryResult; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettings; import net.solarnetwork.central.c2c.domain.CloudIntegrationConfiguration; import net.solarnetwork.central.c2c.domain.CloudIntegrationsConfigurationEntity; +import net.solarnetwork.central.c2c.domain.UserSettingsEntity; import net.solarnetwork.central.domain.BasicClaimableJobState; import net.solarnetwork.central.domain.UserLongCompositePK; import net.solarnetwork.central.domain.UserRelatedCompositeKey; import net.solarnetwork.central.user.c2c.domain.CloudDatumStreamPollTaskEntityInput; import net.solarnetwork.central.user.c2c.domain.CloudDatumStreamPropertyConfigurationInput; import net.solarnetwork.central.user.c2c.domain.CloudIntegrationsConfigurationInput; +import net.solarnetwork.central.user.c2c.domain.UserSettingsEntityInput; import net.solarnetwork.dao.FilterResults; import net.solarnetwork.domain.Result; import net.solarnetwork.domain.datum.Datum; @@ -51,7 +54,7 @@ * Service API for SolarUser cloud integrations support. * * @author matt - * @version 1.2 + * @version 1.3 */ public interface UserCloudIntegrationsBiz { @@ -79,6 +82,35 @@ public interface UserCloudIntegrationsBiz { */ CloudDatumStreamService datumStreamService(String identifier); + /** + * Get the user-level settings. + * + * @param userId + * the user ID + * @return the settings ,or {@literal null} if none exist + * @since 1.3 + */ + UserSettingsEntity settingsForUser(Long userId); + + /** + * Save user-level settings. + * + * @param userId + * the ID of the user to save the settings for + * @param input + * the settings to save + * @return the saved settings + */ + UserSettingsEntity saveSettings(Long userId, UserSettingsEntityInput input); + + /** + * Delete user-level settings. + * + * @param userId + * the ID of the user to delete the settings for + */ + void deleteSettings(Long userId); + /** * Get a list of all available cloud integration configurations for a given * user. @@ -299,4 +331,13 @@ CloudDatumStreamPollTaskEntity saveDatumStreamPollTask(UserLongCompositePK id, */ void deleteDatumStreamPollTask(UserLongCompositePK id); + /** + * Get the default datum stream settings, if no stream-level or user-level + * settings are available. + * + * @return the default cloud datum stream settings + * @since 1.3 + */ + CloudDatumStreamSettings defaultDatumStreamSettings(); + } diff --git a/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/biz/impl/DaoUserCloudIntegrationsBiz.java b/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/biz/impl/DaoUserCloudIntegrationsBiz.java index 0d24d8628..af8dfa23b 100644 --- a/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/biz/impl/DaoUserCloudIntegrationsBiz.java +++ b/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/biz/impl/DaoUserCloudIntegrationsBiz.java @@ -51,8 +51,11 @@ import net.solarnetwork.central.c2c.dao.CloudDatumStreamPollTaskDao; import net.solarnetwork.central.c2c.dao.CloudDatumStreamPollTaskFilter; import net.solarnetwork.central.c2c.dao.CloudDatumStreamPropertyConfigurationDao; +import net.solarnetwork.central.c2c.dao.CloudDatumStreamSettingsEntityDao; import net.solarnetwork.central.c2c.dao.CloudIntegrationConfigurationDao; import net.solarnetwork.central.c2c.dao.CloudIntegrationsFilter; +import net.solarnetwork.central.c2c.dao.UserSettingsEntityDao; +import net.solarnetwork.central.c2c.domain.BasicCloudDatumStreamSettings; import net.solarnetwork.central.c2c.domain.CloudDataValue; import net.solarnetwork.central.c2c.domain.CloudDatumStreamConfiguration; import net.solarnetwork.central.c2c.domain.CloudDatumStreamMappingConfiguration; @@ -60,8 +63,11 @@ import net.solarnetwork.central.c2c.domain.CloudDatumStreamPropertyConfiguration; import net.solarnetwork.central.c2c.domain.CloudDatumStreamQueryFilter; import net.solarnetwork.central.c2c.domain.CloudDatumStreamQueryResult; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettings; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettingsEntity; import net.solarnetwork.central.c2c.domain.CloudIntegrationConfiguration; import net.solarnetwork.central.c2c.domain.CloudIntegrationsConfigurationEntity; +import net.solarnetwork.central.c2c.domain.UserSettingsEntity; import net.solarnetwork.central.dao.UserModifiableEnabledStatusDao; import net.solarnetwork.central.domain.BasicClaimableJobState; import net.solarnetwork.central.domain.UserLongCompositePK; @@ -72,6 +78,7 @@ import net.solarnetwork.central.user.c2c.domain.CloudDatumStreamPollTaskEntityInput; import net.solarnetwork.central.user.c2c.domain.CloudDatumStreamPropertyConfigurationInput; import net.solarnetwork.central.user.c2c.domain.CloudIntegrationsConfigurationInput; +import net.solarnetwork.central.user.c2c.domain.UserSettingsEntityInput; import net.solarnetwork.dao.FilterResults; import net.solarnetwork.dao.FilterableDao; import net.solarnetwork.dao.GenericDao; @@ -84,12 +91,18 @@ * DAO based implementation of {@link UserCloudIntegrationsBiz}. * * @author matt - * @version 1.3 + * @version 1.4 */ public class DaoUserCloudIntegrationsBiz implements UserCloudIntegrationsBiz { + /** The {@code defaultDatumStreamSettings} default value. */ + public static final CloudDatumStreamSettings DEFAULT_DATUM_STREAM_SETTINGS = new BasicCloudDatumStreamSettings( + true, false); + + private final UserSettingsEntityDao userSettingsDao; private final CloudIntegrationConfigurationDao integrationDao; private final CloudDatumStreamConfigurationDao datumStreamDao; + private final CloudDatumStreamSettingsEntityDao datumStreamSettingsDao; private final CloudDatumStreamMappingConfigurationDao datumStreamMappingDao; private final CloudDatumStreamPropertyConfigurationDao datumStreamPropertyDao; private final CloudDatumStreamPollTaskDao datumStreamPollTaskDao; @@ -99,14 +112,19 @@ public class DaoUserCloudIntegrationsBiz implements UserCloudIntegrationsBiz { private final Map> serviceSecureKeys; private Validator validator; + private CloudDatumStreamSettings defaultDatumStreamSettings = DEFAULT_DATUM_STREAM_SETTINGS; /** * Constructor. * + * @param userSettingsDao + * the user settings DAO * @param integrationDao * the configuration DAO * @param datumStreamDao * the datum stream DAO + * @param datumStreamSettingsDao + * the datum stream settings DAO * @param datumStreamMappingDao * the datum stream mapping DAO * @param datumStreamPropertyDao @@ -120,15 +138,20 @@ public class DaoUserCloudIntegrationsBiz implements UserCloudIntegrationsBiz { * @throws IllegalArgumentException * if any argument is {@literal null} */ - public DaoUserCloudIntegrationsBiz(CloudIntegrationConfigurationDao integrationDao, + public DaoUserCloudIntegrationsBiz(UserSettingsEntityDao userSettingsDao, + CloudIntegrationConfigurationDao integrationDao, CloudDatumStreamConfigurationDao datumStreamDao, + CloudDatumStreamSettingsEntityDao datumStreamSettingsDao, CloudDatumStreamMappingConfigurationDao datumStreamMappingDao, CloudDatumStreamPropertyConfigurationDao datumStreamPropertyDao, CloudDatumStreamPollTaskDao datumStreamPollTaskDao, TextEncryptor textEncryptor, Collection integrationServices) { super(); + this.userSettingsDao = requireNonNullArgument(userSettingsDao, "userSettingsDao"); this.integrationDao = requireNonNullArgument(integrationDao, "integrationDao"); this.datumStreamDao = requireNonNullArgument(datumStreamDao, "datumStreamDao"); + this.datumStreamSettingsDao = requireNonNullArgument(datumStreamSettingsDao, + "datumStreamSettingsDao"); this.datumStreamMappingDao = requireNonNullArgument(datumStreamMappingDao, "datumStreamMappingDao"); this.datumStreamPropertyDao = requireNonNullArgument(datumStreamPropertyDao, @@ -167,6 +190,31 @@ public CloudDatumStreamService datumStreamService(String identifier) { return datumStreamServices.get(requireNonNullArgument(identifier, "identifier")); } + @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) + @Override + public UserSettingsEntity settingsForUser(Long userId) { + return userSettingsDao.get(requireNonNullArgument(userId, "userId")); + } + + @Override + public UserSettingsEntity saveSettings(Long userId, UserSettingsEntityInput input) { + UserSettingsEntity entity = requireNonNullArgument(input, "input").toEntity(userId, now()); + return userSettingsDao.get(userSettingsDao.store(entity)); + } + + @Transactional(readOnly = false, propagation = Propagation.REQUIRED) + @Override + public void deleteSettings(Long userId) { + UserSettingsEntity key = new UserSettingsEntity(requireNonNullArgument(userId, "userId"), now()); + userSettingsDao.delete(key); + } + + @Transactional(readOnly = false, propagation = Propagation.REQUIRED) + @Override + public CloudDatumStreamSettings defaultDatumStreamSettings() { + return defaultDatumStreamSettings; + } + @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) @Override public , K extends UserRelatedCompositeKey> FilterResults listConfigurationsForUser( @@ -462,6 +510,8 @@ private , K extends UserRel result = (GenericDao) datumStreamMappingDao; } else if ( CloudDatumStreamPropertyConfiguration.class.isAssignableFrom(clazz) ) { result = (GenericDao) datumStreamPropertyDao; + } else if ( CloudDatumStreamSettingsEntity.class.isAssignableFrom(clazz) ) { + result = (GenericDao) datumStreamSettingsDao; } if ( result != null ) { return result; @@ -498,6 +548,8 @@ private , K extends UserRel result = (FilterableDao) datumStreamMappingDao; } else if ( CloudDatumStreamPropertyConfiguration.class.isAssignableFrom(clazz) ) { result = (FilterableDao) datumStreamPropertyDao; + } else if ( CloudDatumStreamSettingsEntity.class.isAssignableFrom(clazz) ) { + result = (FilterableDao) datumStreamSettingsDao; } if ( result != null ) { return result; @@ -523,4 +575,30 @@ public Validator getValidator() { public void setValidator(Validator validator) { this.validator = validator; } + + /** + * Get the default datum stream settings. + * + * @return the settings, never {@literal null} + * @since 1.4 + */ + public final CloudDatumStreamSettings getDefaultDatumStreamSettings() { + return defaultDatumStreamSettings; + } + + /** + * Set the default datum stream settings. + * + * @param defaultDatumStreamSettings + * the settings to set; if {@code null} then + * {@link #DEFAULT_DATUM_STREAM_SETTINGS} will be used + * @since 1.4 + */ + public final void setDefaultDatumStreamSettings( + CloudDatumStreamSettings defaultDatumStreamSettings) { + this.defaultDatumStreamSettings = (defaultDatumStreamSettings != null + ? defaultDatumStreamSettings + : DEFAULT_DATUM_STREAM_SETTINGS); + } + } diff --git a/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/config/UserCloudIntegrationsBizConfig.java b/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/config/UserCloudIntegrationsBizConfig.java index f48cb6082..b90d5dee9 100644 --- a/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/config/UserCloudIntegrationsBizConfig.java +++ b/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/config/UserCloudIntegrationsBizConfig.java @@ -35,14 +35,16 @@ import net.solarnetwork.central.c2c.dao.CloudDatumStreamMappingConfigurationDao; import net.solarnetwork.central.c2c.dao.CloudDatumStreamPollTaskDao; import net.solarnetwork.central.c2c.dao.CloudDatumStreamPropertyConfigurationDao; +import net.solarnetwork.central.c2c.dao.CloudDatumStreamSettingsEntityDao; import net.solarnetwork.central.c2c.dao.CloudIntegrationConfigurationDao; +import net.solarnetwork.central.c2c.dao.UserSettingsEntityDao; import net.solarnetwork.central.user.c2c.biz.impl.DaoUserCloudIntegrationsBiz; /** * Configuration for user cloud integrations services. * * @author matt - * @version 1.1 + * @version 1.2 */ @Configuration(proxyBeanMethods = false) @Profile(CLOUD_INTEGRATIONS) @@ -63,6 +65,12 @@ public class UserCloudIntegrationsBizConfig { @Autowired private CloudDatumStreamPollTaskDao datumStreamPollTaskDao; + @Autowired + private UserSettingsEntityDao userSettingsDao; + + @Autowired + private CloudDatumStreamSettingsEntityDao datumStreamSettingsDao; + @Autowired private Collection integrationServices; @@ -72,9 +80,9 @@ public class UserCloudIntegrationsBizConfig { @Bean public DaoUserCloudIntegrationsBiz userCloudIntegrationsBiz() { - DaoUserCloudIntegrationsBiz biz = new DaoUserCloudIntegrationsBiz(integrationDao, datumStreamDao, - datumStreamMappingDao, datumStreamPropertyDao, datumStreamPollTaskDao, textEncryptor, - integrationServices); + DaoUserCloudIntegrationsBiz biz = new DaoUserCloudIntegrationsBiz(userSettingsDao, + integrationDao, datumStreamDao, datumStreamSettingsDao, datumStreamMappingDao, + datumStreamPropertyDao, datumStreamPollTaskDao, textEncryptor, integrationServices); return biz; } diff --git a/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/domain/CloudDatumStreamSettingsEntityInput.java b/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/domain/CloudDatumStreamSettingsEntityInput.java new file mode 100644 index 000000000..4a3089c3e --- /dev/null +++ b/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/domain/CloudDatumStreamSettingsEntityInput.java @@ -0,0 +1,88 @@ +/* ================================================================== + * CloudDatumStreamSettingsEntityInput.java - 28/10/2024 4:26:49 pm + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.user.c2c.domain; + +import java.time.Instant; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettingsEntity; +import net.solarnetwork.central.domain.UserLongCompositePK; + +/** + * DTO for datum stream settings entity. + * + * @author matt + * @version 1.0 + */ +public class CloudDatumStreamSettingsEntityInput implements + CloudIntegrationsConfigurationInput { + + private boolean publishToSolarIn = true; + + private boolean publishToSolarFlux = false; + + @Override + public CloudDatumStreamSettingsEntity toEntity(UserLongCompositePK id, Instant date) { + CloudDatumStreamSettingsEntity conf = new CloudDatumStreamSettingsEntity(id, date); + conf.setPublishToSolarIn(publishToSolarIn); + conf.setPublishToSolarFlux(publishToSolarFlux); + return conf; + } + + /** + * Get the "publish to SolarIn" toggle. + * + * @return {@literal true} if data should be published to SolarIn + */ + public boolean isPublishToSolarIn() { + return publishToSolarIn; + } + + /** + * Set the "publish to SolarIn" toggle. + * + * @param publishToSolarIn + * {@literal true} if data should be published to SolarIn + */ + public void setPublishToSolarIn(boolean publishToSolarIn) { + this.publishToSolarIn = publishToSolarIn; + } + + /** + * Get the "publish to SolarFlux" toggle. + * + * @return {@literal true} if data should be published to SolarFlux + */ + public boolean isPublishToSolarFlux() { + return publishToSolarFlux; + } + + /** + * Set the "publish to SolarFlux" toggle. + * + * @param publishToSolarFlux + * {@literal true} if data should be published to SolarFlux + */ + public void setPublishToSolarFlux(boolean publishToSolarFlux) { + this.publishToSolarFlux = publishToSolarFlux; + } + +} diff --git a/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/domain/UserSettingsEntityInput.java b/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/domain/UserSettingsEntityInput.java new file mode 100644 index 000000000..888e90099 --- /dev/null +++ b/solarnet/user-cloud-integrations/src/main/java/net/solarnetwork/central/user/c2c/domain/UserSettingsEntityInput.java @@ -0,0 +1,94 @@ +/* ================================================================== + * UserSettingsEntityInput.java - 28/10/2024 3:32:53 pm + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.user.c2c.domain; + +import java.time.Instant; +import net.solarnetwork.central.c2c.domain.UserSettingsEntity; + +/** + * DTO for user settings entity. + * + * @author matt + * @version 1.0 + */ +public class UserSettingsEntityInput { + + private boolean publishToSolarIn = true; + + private boolean publishToSolarFlux = false; + + /** + * Create an entity from the input properties and a given primary key. + * + * @param userId + * the primary key to use + * @param date + * the creation date to use + * @return the new entity + */ + public UserSettingsEntity toEntity(Long userId, Instant date) { + UserSettingsEntity conf = new UserSettingsEntity(userId, date); + conf.setPublishToSolarIn(publishToSolarIn); + conf.setPublishToSolarFlux(publishToSolarFlux); + return conf; + } + + /** + * Get the "publish to SolarIn" toggle. + * + * @return {@literal true} if data should be published to SolarIn + */ + public boolean isPublishToSolarIn() { + return publishToSolarIn; + } + + /** + * Set the "publish to SolarIn" toggle. + * + * @param publishToSolarIn + * {@literal true} if data should be published to SolarIn + */ + public void setPublishToSolarIn(boolean publishToSolarIn) { + this.publishToSolarIn = publishToSolarIn; + } + + /** + * Get the "publish to SolarFlux" toggle. + * + * @return {@literal true} if data should be published to SolarFlux + */ + public boolean isPublishToSolarFlux() { + return publishToSolarFlux; + } + + /** + * Set the "publish to SolarFlux" toggle. + * + * @param publishToSolarFlux + * {@literal true} if data should be published to SolarFlux + */ + public void setPublishToSolarFlux(boolean publishToSolarFlux) { + this.publishToSolarFlux = publishToSolarFlux; + } + +} diff --git a/solarnet/user-cloud-integrations/src/test/java/net/solarnetwork/central/user/c2c/biz/impl/test/DaoUserCloudIntegrationsBizTests.java b/solarnet/user-cloud-integrations/src/test/java/net/solarnetwork/central/user/c2c/biz/impl/test/DaoUserCloudIntegrationsBizTests.java index d91d37a72..027ffd39e 100644 --- a/solarnet/user-cloud-integrations/src/test/java/net/solarnetwork/central/user/c2c/biz/impl/test/DaoUserCloudIntegrationsBizTests.java +++ b/solarnet/user-cloud-integrations/src/test/java/net/solarnetwork/central/user/c2c/biz/impl/test/DaoUserCloudIntegrationsBizTests.java @@ -25,6 +25,7 @@ import static java.time.Instant.now; import static net.solarnetwork.central.domain.BasicClaimableJobState.Completed; import static net.solarnetwork.central.domain.BasicClaimableJobState.Queued; +import static net.solarnetwork.central.test.CommonTestUtils.randomBoolean; import static net.solarnetwork.central.test.CommonTestUtils.randomDecimal; import static net.solarnetwork.central.test.CommonTestUtils.randomInt; import static net.solarnetwork.central.test.CommonTestUtils.randomLong; @@ -61,12 +62,18 @@ import net.solarnetwork.central.c2c.dao.CloudDatumStreamMappingConfigurationDao; import net.solarnetwork.central.c2c.dao.CloudDatumStreamPollTaskDao; import net.solarnetwork.central.c2c.dao.CloudDatumStreamPropertyConfigurationDao; +import net.solarnetwork.central.c2c.dao.CloudDatumStreamSettingsEntityDao; import net.solarnetwork.central.c2c.dao.CloudIntegrationConfigurationDao; +import net.solarnetwork.central.c2c.dao.UserSettingsEntityDao; +import net.solarnetwork.central.c2c.domain.BasicCloudDatumStreamSettings; import net.solarnetwork.central.c2c.domain.CloudDatumStreamConfiguration; import net.solarnetwork.central.c2c.domain.CloudDatumStreamMappingConfiguration; import net.solarnetwork.central.c2c.domain.CloudDatumStreamPollTaskEntity; import net.solarnetwork.central.c2c.domain.CloudDatumStreamPropertyConfiguration; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettings; +import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettingsEntity; import net.solarnetwork.central.c2c.domain.CloudIntegrationConfiguration; +import net.solarnetwork.central.c2c.domain.UserSettingsEntity; import net.solarnetwork.central.domain.BasicClaimableJobState; import net.solarnetwork.central.domain.UserLongCompositePK; import net.solarnetwork.central.domain.UserLongIntegerCompositePK; @@ -77,6 +84,7 @@ import net.solarnetwork.central.user.c2c.domain.CloudDatumStreamPollTaskEntityInput; import net.solarnetwork.central.user.c2c.domain.CloudDatumStreamPropertyConfigurationInput; import net.solarnetwork.central.user.c2c.domain.CloudIntegrationConfigurationInput; +import net.solarnetwork.central.user.c2c.domain.UserSettingsEntityInput; import net.solarnetwork.dao.BasicFilterResults; import net.solarnetwork.dao.Entity; import net.solarnetwork.dao.FilterResults; @@ -89,7 +97,7 @@ * Test cases for the {@link DaoUserCloudIntegrationsBiz} class. * * @author matt - * @version 1.1 + * @version 1.2 */ @SuppressWarnings("static-access") @ExtendWith(MockitoExtension.class) @@ -115,6 +123,12 @@ public class DaoUserCloudIntegrationsBizTests { @Mock private CloudIntegrationService integrationService; + @Mock + private UserSettingsEntityDao userSettingsDao; + + @Mock + private CloudDatumStreamSettingsEntityDao datumStreamSettingsDao; + @Captor private ArgumentCaptor localeCaptor; @@ -133,6 +147,12 @@ public class DaoUserCloudIntegrationsBizTests { @Captor private ArgumentCaptor datumStreamPollTaskCaptor; + @Captor + private ArgumentCaptor userSettingsCaptor; + + @Captor + private ArgumentCaptor datumStreamSettingsCaptor; + @Captor private ArgumentCaptor filterCaptor; @@ -144,15 +164,16 @@ public class DaoUserCloudIntegrationsBizTests { @BeforeEach public void setup() { given(integrationService.getId()).willReturn(TEST_SERVICE_ID); + given(integrationService.getSettingUid()).willReturn(TEST_SERVICE_ID); // provide settings to verify masking sensitive values List settings = Arrays.asList(new BasicTextFieldSettingSpecifier("foo", null), new BasicTextFieldSettingSpecifier("watchout", null, true)); given(integrationService.getSettingSpecifiers()).willReturn(settings); - biz = new DaoUserCloudIntegrationsBiz(integrationDao, datumStreamDao, datumStreamMappingDao, - datumStreamPropertyDao, datumStreamPollTaskDao, textEncryptor, - Collections.singleton(integrationService)); + biz = new DaoUserCloudIntegrationsBiz(userSettingsDao, integrationDao, datumStreamDao, + datumStreamSettingsDao, datumStreamMappingDao, datumStreamPropertyDao, + datumStreamPollTaskDao, textEncryptor, Collections.singleton(integrationService)); ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); biz.setValidator(factory.getValidator()); @@ -939,4 +960,84 @@ public void datumStreamPollTaskEntity_delete() { // @formatter:on } + @Test + public void defaultDatumStreamSettings() { + // GIVEN + BasicCloudDatumStreamSettings defaults = new BasicCloudDatumStreamSettings(false, false); + biz.setDefaultDatumStreamSettings(defaults); + + // WHEN + CloudDatumStreamSettings result = biz.defaultDatumStreamSettings(); + + // THEN + and.then(result).as("Configured defaults returned").isSameAs(defaults); + } + + @Test + public void userSettings() { + // GIVEN + Long userId = randomLong(); + + UserSettingsEntity entity = new UserSettingsEntity(userId, now()); + given(userSettingsDao.get(userId)).willReturn(entity); + + // WHEN + UserSettingsEntity result = biz.settingsForUser(userId); + + // THEN + and.then(result).as("Result from DAO returned").isSameAs(entity); + } + + @Test + public void saveUserSettigns() { + // GIVEN + Long userId = randomLong(); + given(userSettingsDao.store(any())).willReturn(userId); + + UserSettingsEntity entity = new UserSettingsEntity(userId, now()); + given(userSettingsDao.get(userId)).willReturn(entity); + + // WHEN + UserSettingsEntityInput input = new UserSettingsEntityInput(); + input.setPublishToSolarIn(randomBoolean()); + input.setPublishToSolarFlux(randomBoolean()); + UserSettingsEntity result = biz.saveSettings(userId, input); + + // THEN + // @formatter:off + then(userSettingsDao).should().store(userSettingsCaptor.capture()); + and.then(userSettingsCaptor.getValue()) + .as("User ID as provided") + .returns(userId, from(UserSettingsEntity::getUserId)) + .as("Publish SolarIn as provided") + .returns(input.isPublishToSolarIn(), from(UserSettingsEntity::isPublishToSolarIn)) + .as("Publish SolarFlux as provided") + .returns(input.isPublishToSolarFlux(), from(UserSettingsEntity::isPublishToSolarFlux)) + ; + + and.then(result) + .as("Result from DAO returned") + .isSameAs(entity) + ; + // @formatter:on + } + + @Test + public void deleteUserSettigns() { + // GIVEN + Long userId = randomLong(); + + // WHEN + biz.deleteSettings(userId); + + // THEN + // @formatter:off + then(userSettingsDao).should().delete(userSettingsCaptor.capture()); + and.then(userSettingsCaptor.getValue()) + .as("User ID as provided") + .returns(userId, from(UserSettingsEntity::getUserId)) + ; + // @formatter:on + } + } From 6a7c3d806416f25f892fcf6100f383c1f3c3fafd Mon Sep 17 00:00:00 2001 From: Matt Magoffin Date: Mon, 28 Oct 2024 17:19:57 +1300 Subject: [PATCH 02/11] NET-414: add SolarFlux publishing to datum stream poll task. --- .../impl/DaoCloudDatumStreamPollService.java | 35 +++++ .../DaoCloudDatumStreamPollServiceTests.java | 143 ++++++++++++++++++ ...loudIntegrationsDatumStreamPollConfig.java | 6 + 3 files changed, 184 insertions(+) diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/DaoCloudDatumStreamPollService.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/DaoCloudDatumStreamPollService.java index 252b55c09..bf7ab309c 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/DaoCloudDatumStreamPollService.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/DaoCloudDatumStreamPollService.java @@ -60,7 +60,10 @@ import net.solarnetwork.central.c2c.domain.CloudDatumStreamSettings; import net.solarnetwork.central.c2c.domain.CloudIntegrationsUserEvents; import net.solarnetwork.central.dao.SolarNodeOwnershipDao; +import net.solarnetwork.central.datum.biz.DatumProcessor; +import net.solarnetwork.central.datum.domain.GeneralNodeDatum; import net.solarnetwork.central.datum.domain.GeneralObjectDatum; +import net.solarnetwork.central.datum.support.DatumUtils; import net.solarnetwork.central.datum.v2.dao.DatumEntity; import net.solarnetwork.central.datum.v2.dao.DatumWriteOnlyDao; import net.solarnetwork.central.domain.BasicClaimableJobState; @@ -98,6 +101,7 @@ public class DaoCloudDatumStreamPollService private final Function datumStreamServiceProvider; private Duration shutdownMaxWait = DEFAULT_SHUTDOWN_MAX_WAIT; private CloudDatumStreamSettings defaultDatumStreamSettings = DEFAULT_DATUM_STREAM_SETTINGS; + private DatumProcessor fluxPublisher; /** * Constructor. @@ -351,6 +355,7 @@ private CloudDatumStreamPollTaskEntity executeTask() throws Exception { if ( polledDatum != null && !polledDatum.isEmpty() ) { log.debug("Polling for {} found {} datum to import", datumStreamIdent, polledDatum.size()); + final DatumProcessor fluxPublisher = getFluxPublisher(); for ( var datum : polledDatum ) { if ( datum instanceof DatumEntity d ) { if ( datumStreamSettings.isPublishToSolarIn() ) { @@ -360,10 +365,21 @@ private CloudDatumStreamPollTaskEntity executeTask() throws Exception { if ( datumStreamSettings.isPublishToSolarIn() ) { datumDao.persist(d); } + if ( fluxPublisher != null && datumStreamSettings.isPublishToSolarFlux() + && datum instanceof GeneralNodeDatum nodeDatum ) { + fluxPublisher.processDatum(nodeDatum); + } } else { if ( datumStreamSettings.isPublishToSolarIn() ) { datumDao.store(datum); } + if ( fluxPublisher != null && datumStreamSettings.isPublishToSolarFlux() + && datum.getKind() == ObjectDatumKind.Node ) { + GeneralObjectDatum gd = DatumUtils.convertGeneralDatum(datum); + if ( gd instanceof GeneralNodeDatum nodeDatum ) { + fluxPublisher.processDatum(nodeDatum); + } + } } if ( lastDatumDate == null || lastDatumDate.isBefore(datum.getTimestamp()) ) { lastDatumDate = datum.getTimestamp(); @@ -482,4 +498,23 @@ public final void setDefaultDatumStreamSettings( : DEFAULT_DATUM_STREAM_SETTINGS); } + /** + * Get the SolarFlux publisher. + * + * @return the publisher, or {@literal null} + */ + public DatumProcessor getFluxPublisher() { + return fluxPublisher; + } + + /** + * Set the SolarFlux publisher. + * + * @param fluxPublisher + * the publisher to set + */ + public void setFluxPublisher(DatumProcessor fluxPublisher) { + this.fluxPublisher = fluxPublisher; + } + } diff --git a/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/biz/impl/test/DaoCloudDatumStreamPollServiceTests.java b/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/biz/impl/test/DaoCloudDatumStreamPollServiceTests.java index 1e932d806..0d66eb254 100644 --- a/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/biz/impl/test/DaoCloudDatumStreamPollServiceTests.java +++ b/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/biz/impl/test/DaoCloudDatumStreamPollServiceTests.java @@ -34,12 +34,14 @@ import static net.solarnetwork.domain.datum.DatumId.nodeId; import static org.assertj.core.api.BDDAssertions.and; import static org.assertj.core.api.BDDAssertions.from; +import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; +import static org.mockito.internal.verification.VerificationModeFactory.times; import java.time.Clock; import java.time.Instant; import java.time.ZoneOffset; @@ -68,14 +70,19 @@ import net.solarnetwork.central.c2c.dao.CloudDatumStreamPollTaskDao; import net.solarnetwork.central.c2c.dao.CloudDatumStreamSettingsEntityDao; import net.solarnetwork.central.c2c.domain.BasicCloudDatumStreamQueryResult; +import net.solarnetwork.central.c2c.domain.BasicCloudDatumStreamSettings; import net.solarnetwork.central.c2c.domain.CloudDatumStreamConfiguration; import net.solarnetwork.central.c2c.domain.CloudDatumStreamPollTaskEntity; import net.solarnetwork.central.c2c.domain.CloudDatumStreamQueryFilter; import net.solarnetwork.central.c2c.domain.CloudIntegrationsUserEvents; import net.solarnetwork.central.dao.SolarNodeOwnershipDao; +import net.solarnetwork.central.datum.biz.DatumProcessor; +import net.solarnetwork.central.datum.domain.GeneralNodeDatum; +import net.solarnetwork.central.datum.domain.GeneralNodeDatumPK; import net.solarnetwork.central.datum.v2.dao.DatumWriteOnlyDao; import net.solarnetwork.central.datum.v2.domain.DatumPK; import net.solarnetwork.central.domain.BasicSolarNodeOwnership; +import net.solarnetwork.domain.Identity; import net.solarnetwork.domain.datum.Datum; import net.solarnetwork.domain.datum.DatumSamples; import net.solarnetwork.domain.datum.GeneralDatum; @@ -120,6 +127,9 @@ public class DaoCloudDatumStreamPollServiceTests { @Mock private ExecutorService executor; + @Mock + private DatumProcessor fluxProcessor; + @Captor private ArgumentCaptor queryFilterCaptor; @@ -129,6 +139,9 @@ public class DaoCloudDatumStreamPollServiceTests { @Captor private ArgumentCaptor datumCaptor; + @Captor + private ArgumentCaptor> generalNodeDatumCaptor; + private DaoCloudDatumStreamPollService service; @BeforeEach @@ -141,6 +154,8 @@ public void setup() { service = new DaoCloudDatumStreamPollService(clock, userEventAppenderBiz, nodeOwnershipDao, taskDao, datumStreamDao, datumStreamSettingsDao, datumDao, executor, datumStreamServices::get); + service.setFluxPublisher(fluxProcessor); + } @Test @@ -408,4 +423,132 @@ public void executeTask_shutdown() throws Exception { // @formatter:on } + @Test + public void executeTask_fluxPublish() throws Exception { + // GIVEN + // submit task + var future = new CompletableFuture(); + given(executor.submit(argThat((Callable call) -> { + try { + future.complete(call.call()); + } catch ( Exception e ) { + future.completeExceptionally(e); + } + return true; + }))).willReturn(future); + + final Instant hour = clock.instant().truncatedTo(ChronoUnit.HOURS); + + final CloudDatumStreamConfiguration datumStream = new CloudDatumStreamConfiguration(TEST_USER_ID, + randomLong(), now()); + datumStream.setDatumStreamMappingId(randomLong()); + datumStream.setServiceIdentifier(TEST_DATUM_STREAM_SERVICE_IDENTIFIER); + datumStream.setSchedule("0 0/5 * * * *"); + datumStream.setKind(ObjectDatumKind.Node); + datumStream.setObjectId(randomLong()); + datumStream.setSourceId(randomString()); + + // look up datum stream associated with task + given(datumStreamDao.get(datumStream.getId())).willReturn(datumStream); + + // resolve datum stream settings (SolarIn OFF, SolarFlux ON) + BasicCloudDatumStreamSettings datumStreamSettings = new BasicCloudDatumStreamSettings(false, + true); + given(datumStreamSettingsDao.resolveSettings(TEST_USER_ID, datumStream.getConfigId(), + DEFAULT_DATUM_STREAM_SETTINGS)).willReturn(datumStreamSettings); + + // verify node ownership + final var nodeOwner = new BasicSolarNodeOwnership(datumStream.getObjectId(), TEST_USER_ID, "NZ", + UTC, true, false); + given(nodeOwnershipDao.ownershipForNodeId(datumStream.getObjectId())).willReturn(nodeOwner); + + // update task state to "processing" + given(taskDao.updateTaskState(datumStream.getId(), Executing, Claimed)).willReturn(true); + + // query for data associated with service configured on datum stream + // here we return a datum for "5 min ago" + final Datum datum1 = new GeneralDatum( + nodeId(datumStream.getObjectId(), datumStream.getSourceId(), hour.minusSeconds(300)), + new DatumSamples(Map.of("watts", 123), Map.of("wattHours", 23456L), null)); + final Datum datum2 = new GeneralDatum( + nodeId(datumStream.getObjectId(), datumStream.getSourceId(), hour.minusSeconds(120)), + new DatumSamples(Map.of("watts", 234), Map.of("wattHours", 34567L), null)); + given(datumStreamService.datum(same(datumStream), any())) + .willReturn(new BasicCloudDatumStreamQueryResult(List.of(datum1, datum2))); + + // post datum to SolarFlux + given(fluxProcessor.processDatum(any())).willReturn(true); + + // update task details + given(taskDao.updateTask(any(), eq(Executing))).willReturn(true); + + // WHEN + var task = new CloudDatumStreamPollTaskEntity(datumStream.getId()); + task.setState(Claimed); + task.setExecuteAt(hour); + task.setStartAt(hour.minusSeconds(300)); + + Future result = service.executeTask(task); + CloudDatumStreamPollTaskEntity resultTask = result.get(1, TimeUnit.MINUTES); + + // THEN + // @formatter:off + then(datumStreamService).should().datum(same(datumStream), queryFilterCaptor.capture()); + and.then(queryFilterCaptor.getValue()) + .as("The query start date is the startAt of the task") + .returns(task.getStartAt(), from(CloudDatumStreamQueryFilter::getStartDate)) + .as("The query end date is the current date") + .returns(clock.instant(), from(CloudDatumStreamQueryFilter::getEndDate)) + ; + + then(taskDao).should().updateTask(taskCaptor.capture(), eq(Executing)); + and.then(taskCaptor.getValue()) + .as("Task to update is copy of given task") + .isNotSameAs(task) + .as("Task to update has same ID as given task") + .isEqualTo(task) + .as("Update task state to Queued to run again") + .returns(Queued, from(CloudDatumStreamPollTaskEntity::getState)) + .as("Update task execute date to next time based on configuration schedule (every 5min)") + .returns(task.getExecuteAt().plusSeconds(300), from(CloudDatumStreamPollTaskEntity::getExecuteAt)) + .as("Update task start date to highest date of datum captured") + .returns(datum2.getTimestamp(), from(CloudDatumStreamPollTaskEntity::getStartAt)) + .as("No message generated for successful execution") + .returns(null, from(CloudDatumStreamPollTaskEntity::getMessage)) + .as("No service properties generated for successful execution") + .returns(null, from(CloudDatumStreamPollTaskEntity::getServiceProperties)) + ; + + and.then(resultTask) + .as("Result task is same as passed to DAO for update") + .isSameAs(taskCaptor.getValue()) + ; + + then(fluxProcessor).should(times(2)).processDatum(generalNodeDatumCaptor.capture()); + and.then(generalNodeDatumCaptor.getAllValues()) + .as("Both datum posted to SolarFlux") + .hasSize(2) + .satisfies(list -> { + and.then(list) + .element(0, type(GeneralNodeDatum.class)) + .as("GeneralNodeDatum ID derived from datum") + .returns(new GeneralNodeDatumPK(datum1.getObjectId(), datum1.getTimestamp(), datum1.getSourceId()), + from(GeneralNodeDatum::getId)) + .as("GeneralNodeDatum properties derviced from datum") + .returns(datum1.getSampleData(), from(GeneralNodeDatum::getSampleData)) + ; + and.then(list) + .element(1, type(GeneralNodeDatum.class)) + .as("GeneralNodeDatum ID derived from datum") + .returns(new GeneralNodeDatumPK(datum2.getObjectId(), datum2.getTimestamp(), datum2.getSourceId()), + from(GeneralNodeDatum::getId)) + .as("GeneralNodeDatum properties derviced from datum") + .returns(datum2.getSampleData(), from(GeneralNodeDatum::getSampleData)) + ; + }) + ; + + // @formatter:on + } + } diff --git a/solarnet/solarjobs/src/main/java/net/solarnetwork/central/jobs/config/CloudIntegrationsDatumStreamPollConfig.java b/solarnet/solarjobs/src/main/java/net/solarnetwork/central/jobs/config/CloudIntegrationsDatumStreamPollConfig.java index 307d155a0..bd228341f 100644 --- a/solarnet/solarjobs/src/main/java/net/solarnetwork/central/jobs/config/CloudIntegrationsDatumStreamPollConfig.java +++ b/solarnet/solarjobs/src/main/java/net/solarnetwork/central/jobs/config/CloudIntegrationsDatumStreamPollConfig.java @@ -43,6 +43,7 @@ import net.solarnetwork.central.c2c.dao.CloudDatumStreamPollTaskDao; import net.solarnetwork.central.c2c.dao.CloudDatumStreamSettingsEntityDao; import net.solarnetwork.central.dao.SolarNodeOwnershipDao; +import net.solarnetwork.central.datum.biz.DatumProcessor; import net.solarnetwork.central.datum.v2.dao.DatumWriteOnlyDao; /** @@ -73,6 +74,10 @@ public class CloudIntegrationsDatumStreamPollConfig implements SolarNetCloudInte @Autowired private DatumWriteOnlyDao datumWriteOnlyDao; + @Autowired(required = false) + @Qualifier("solarflux") + private DatumProcessor fluxPublisher; + @ConfigurationProperties(prefix = "app.c2c.ds-poll.executor") @Qualifier(CLOUD_INTEGRATIONS_POLL) @Bean @@ -92,6 +97,7 @@ public CloudDatumStreamPollService cloudDatumStreamPollService( var service = new DaoCloudDatumStreamPollService(Clock.systemUTC(), userEventAppenderBiz, nodeOwnershipDao, taskDao, datumStreamDao, datumStreamSettingsDao, datumWriteOnlyDao, taskExecutor.getThreadPoolExecutor(), dsMap::get); + service.setFluxPublisher(fluxPublisher); return service; } From 9fdde2464bf1c3816753b97d80837f6436fdb390 Mon Sep 17 00:00:00 2001 From: Matt Magoffin Date: Mon, 28 Oct 2024 17:40:11 +1300 Subject: [PATCH 03/11] If reset poll task due to exception, bump executeAt date into future so task does not immediately try again. --- .../central/c2c/biz/impl/DaoCloudDatumStreamPollService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/DaoCloudDatumStreamPollService.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/DaoCloudDatumStreamPollService.java index bf7ab309c..7feaa7536 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/DaoCloudDatumStreamPollService.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/DaoCloudDatumStreamPollService.java @@ -239,6 +239,11 @@ public CloudDatumStreamPollTaskEntity call() throws Exception { "Resetting datum stream {} poll task by changing state from {} to {} after error: {}", taskInfo.getId().ident(), oldState, Queued, e.toString()); taskInfo.setState(Queued); + if ( Duration.between(taskInfo.getExecuteAt(), clock.instant()) + .getSeconds() < 60 ) { + // bump date into future by 1 minute so we do not immediately try to process again + taskInfo.setExecuteAt(clock.instant().plusSeconds(60)); + } } userEventAppenderBiz.addEvent(taskInfo.getUserId(), eventForConfiguration(taskInfo.getId(), POLL_ERROR_TAGS, errMsg, errData)); From 94659c57812bae8feec7c546fe0d963270b4f613 Mon Sep 17 00:00:00 2001 From: Matt Magoffin Date: Mon, 28 Oct 2024 17:41:40 +1300 Subject: [PATCH 04/11] Fix broken test. --- .../c2c/dao/jdbc/test/JdbcUserSettingsEntityDaoTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/dao/jdbc/test/JdbcUserSettingsEntityDaoTests.java b/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/dao/jdbc/test/JdbcUserSettingsEntityDaoTests.java index b2fd060a7..4f3c98c40 100644 --- a/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/dao/jdbc/test/JdbcUserSettingsEntityDaoTests.java +++ b/solarnet/cloud-integrations/src/test/java/net/solarnetwork/central/c2c/dao/jdbc/test/JdbcUserSettingsEntityDaoTests.java @@ -81,8 +81,8 @@ public void insert() { .containsEntry("user_id", userId) .as("Row creation date") .containsEntry("created", Timestamp.from(conf.getCreated())) - .as("Row modification date") - .containsEntry("modified", Timestamp.from(conf.getModified())) + .as("Row modification date (from creation)") + .containsEntry("modified", Timestamp.from(conf.getCreated())) .as("Row publish SolarIn") .containsEntry("pub_in", conf.isPublishToSolarIn()) .as("Row publish SolarFlux") From 9b6fbe667f9e4becf80a4cada707daf8c736b180 Mon Sep 17 00:00:00 2001 From: Matt Magoffin Date: Mon, 28 Oct 2024 20:12:59 +1300 Subject: [PATCH 05/11] NET-415: start work to track HTTP response lengths. --- .../c2c/http/RestOperationsHelper.java | 36 ++++- ...hTrackingClientHttpRequestInterceptor.java | 130 ++++++++++++++++++ .../central/jobs/config/HttpClientConfig.java | 13 +- 3 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 solarnet/common/src/main/java/net/solarnetwork/central/web/support/ContentLengthTrackingClientHttpRequestInterceptor.java diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/http/RestOperationsHelper.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/http/RestOperationsHelper.java index c7132d360..28dfec14a 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/http/RestOperationsHelper.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/http/RestOperationsHelper.java @@ -27,6 +27,7 @@ import static net.solarnetwork.util.ObjectUtils.requireNonNullArgument; import java.net.URI; import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import org.slf4j.Logger; import org.springframework.http.HttpEntity; @@ -38,11 +39,13 @@ import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestClientResponseException; import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; import org.springframework.web.client.UnknownContentTypeException; import net.solarnetwork.central.biz.UserEventAppenderBiz; import net.solarnetwork.central.c2c.domain.CloudIntegrationsConfigurationEntity; import net.solarnetwork.central.c2c.domain.CloudIntegrationsUserEvents; import net.solarnetwork.central.domain.UserRelatedCompositeKey; +import net.solarnetwork.central.web.support.ContentLengthTrackingClientHttpRequestInterceptor; import net.solarnetwork.service.RemoteServiceException; /** @@ -71,6 +74,8 @@ public class RestOperationsHelper implements CloudIntegrationsUserEvents { /** The sensitive key provider. */ protected final Function> sensitiveKeyProvider; + protected final ThreadLocal responseLengthTracker; + /** * Constructor. * @@ -99,6 +104,20 @@ public RestOperationsHelper(Logger log, UserEventAppenderBiz userEventAppenderBi this.errorEventTags = requireNonNullArgument(errorEventTags, "errorEventTags"); this.encryptor = requireNonNullArgument(encryptor, "encryptor"); this.sensitiveKeyProvider = requireNonNullArgument(sensitiveKeyProvider, "sensitiveKeyProvider"); + + // look for a ContentLengthTrackingClientHttpRequestInterceptor to track response body length with + ThreadLocal tracker = null; + if ( restOps instanceof RestTemplate rt ) { + var interceptors = rt.getInterceptors(); + if ( interceptors != null ) { + for ( var interceptor : interceptors ) { + if ( interceptor instanceof ContentLengthTrackingClientHttpRequestInterceptor t ) { + tracker = t.countThreadLocal(); + } + } + } + } + this.responseLengthTracker = tracker; } /** @@ -168,10 +187,14 @@ public , K extends Us String description, HttpMethod method, B body, C configuration, Class responseType, Function setup, Function, T> handler) { requireNonNullArgument(configuration, "configuration"); - final var headers = new HttpHeaders(); - final URI uri = setup.apply(headers); - final var req = new HttpEntity(body, headers); + if ( responseLengthTracker != null ) { + responseLengthTracker.get().set(0); + } + URI uri = null; try { + final var headers = new HttpHeaders(); + final var req = new HttpEntity(body, headers); + uri = setup.apply(headers); final ResponseEntity res = restOps.exchange(uri, method, req, responseType); return handler.apply(res); } catch ( ResourceAccessException e ) { @@ -217,6 +240,13 @@ public , K extends Us userEventAppenderBiz.addEvent(configuration.getUserId(), eventForConfiguration(configuration, errorEventTags, format("Unknown error: %s", e.toString()))); throw e; + } finally { + if ( responseLengthTracker != null ) { + long len = responseLengthTracker.get().get(); + log.info("[{}] for {} {} tracked {} response body length: {}", description, + configuration.getClass().getSimpleName(), configuration.getId().ident(), uri, + len); + } } } diff --git a/solarnet/common/src/main/java/net/solarnetwork/central/web/support/ContentLengthTrackingClientHttpRequestInterceptor.java b/solarnet/common/src/main/java/net/solarnetwork/central/web/support/ContentLengthTrackingClientHttpRequestInterceptor.java new file mode 100644 index 000000000..41237092a --- /dev/null +++ b/solarnet/common/src/main/java/net/solarnetwork/central/web/support/ContentLengthTrackingClientHttpRequestInterceptor.java @@ -0,0 +1,130 @@ +/* ================================================================== + * ContentLengthTrackingClientHttpRequestInterceptor.java - 28/10/2024 5:53:50 pm + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.web.support; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.commons.io.input.CountingInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import net.solarnetwork.util.ObjectUtils; + +/** + * Client HTTP request interceptor to track the number of bytes processed in the + * response. + * + * @author matt + * @version 1.0 + */ +public class ContentLengthTrackingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { + + private static final Logger log = LoggerFactory + .getLogger(ContentLengthTrackingClientHttpRequestInterceptor.class); + + private final ThreadLocal countThreadLocal; + + /** + * Constructor. + */ + public ContentLengthTrackingClientHttpRequestInterceptor(ThreadLocal countThreadLocal) { + super(); + this.countThreadLocal = countThreadLocal; + } + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution) throws IOException { + ClientHttpResponse response = execution.execute(request, body); + return new ResponseBodyLengthTrackingClientHttpResponse(response); + } + + private class ResponseBodyLengthTrackingClientHttpResponse implements ClientHttpResponse { + + private final ClientHttpResponse delegate; + private CountingInputStream countingBody; + + private ResponseBodyLengthTrackingClientHttpResponse(ClientHttpResponse delegate) { + super(); + this.delegate = ObjectUtils.requireNonNullArgument(delegate, "delegate"); + } + + @Override + public HttpHeaders getHeaders() { + return delegate.getHeaders(); + } + + @Override + public InputStream getBody() throws IOException { + CountingInputStream is = new CountingInputStream(delegate.getBody()); + countingBody = is; + return is; + } + + @Override + public HttpStatusCode getStatusCode() throws IOException { + return delegate.getStatusCode(); + } + + @SuppressWarnings("removal") + @Override + public int getRawStatusCode() throws IOException { + return delegate.getRawStatusCode(); + } + + @Override + public String getStatusText() throws IOException { + return delegate.getStatusText(); + } + + @Override + public void close() { + delegate.close(); + final CountingInputStream is = this.countingBody; + if ( is != null ) { + long count = is.getByteCount(); + if ( count > 0 ) { + log.trace("Adding {} to response input stream body length", count); + countThreadLocal.get().addAndGet(is.getByteCount()); + } + } + } + + } + + /** + * Get the thread-local length tracker. + * + * @return the length tracker + */ + public final ThreadLocal countThreadLocal() { + return countThreadLocal; + } + +} diff --git a/solarnet/solarjobs/src/main/java/net/solarnetwork/central/jobs/config/HttpClientConfig.java b/solarnet/solarjobs/src/main/java/net/solarnetwork/central/jobs/config/HttpClientConfig.java index 670b98369..40dfa4511 100644 --- a/solarnet/solarjobs/src/main/java/net/solarnetwork/central/jobs/config/HttpClientConfig.java +++ b/solarnet/solarjobs/src/main/java/net/solarnetwork/central/jobs/config/HttpClientConfig.java @@ -23,7 +23,8 @@ package net.solarnetwork.central.jobs.config; import java.time.Duration; -import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; @@ -39,6 +40,7 @@ import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; +import net.solarnetwork.central.web.support.ContentLengthTrackingClientHttpRequestInterceptor; import net.solarnetwork.web.jakarta.support.LoggingHttpRequestInterceptor; /** @@ -170,7 +172,10 @@ public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory( @Profile("!http-trace") @Bean public RestTemplate restTemplate(ClientHttpRequestFactory reqFactory) { - return new RestTemplate(reqFactory); + ThreadLocal tl = ThreadLocal.withInitial(AtomicLong::new); + RestTemplate ops = new RestTemplate(reqFactory); + ops.setInterceptors(List.of(new ContentLengthTrackingClientHttpRequestInterceptor(tl))); + return ops; } /** @@ -183,8 +188,10 @@ public RestTemplate restTemplate(ClientHttpRequestFactory reqFactory) { @Profile("http-trace") @Bean public RestTemplate testingRestTemplate(ClientHttpRequestFactory reqFactory) { + ThreadLocal tl = ThreadLocal.withInitial(AtomicLong::new); RestTemplate debugTemplate = new RestTemplate(new BufferingClientHttpRequestFactory(reqFactory)); - debugTemplate.setInterceptors(Arrays.asList(new LoggingHttpRequestInterceptor())); + debugTemplate.setInterceptors(List.of(new ContentLengthTrackingClientHttpRequestInterceptor(tl), + new LoggingHttpRequestInterceptor())); return debugTemplate; } From a7ace231c315cefa6ef0bfad44c3869e156bb8df Mon Sep 17 00:00:00 2001 From: Matt Magoffin Date: Tue, 29 Oct 2024 10:49:52 +1300 Subject: [PATCH 06/11] Fix JavaDoc errors. --- .../central/c2c/dao/jdbc/sql/DeleteUserSettingsEntity.java | 4 ++-- .../central/c2c/dao/jdbc/sql/SelectUserSettingsEntity.java | 4 ++-- .../central/c2c/domain/CloudDatumStreamSettingsEntity.java | 6 ++---- .../central/security/ClientAccessTokenEntity.java | 2 +- .../http/ExternalSystemClientRegistrationRepository.java | 2 -- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/DeleteUserSettingsEntity.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/DeleteUserSettingsEntity.java index 6fb48d677..b6195f099 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/DeleteUserSettingsEntity.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/DeleteUserSettingsEntity.java @@ -48,8 +48,8 @@ public class DeleteUserSettingsEntity implements PreparedStatementCreator, SqlPr /** * Constructor. * - * @param filter - * the filter + * @param userId + * the user ID */ public DeleteUserSettingsEntity(Long userId) { super(); diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/SelectUserSettingsEntity.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/SelectUserSettingsEntity.java index e287e7f48..7e567d806 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/SelectUserSettingsEntity.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/dao/jdbc/sql/SelectUserSettingsEntity.java @@ -50,8 +50,8 @@ public class SelectUserSettingsEntity implements PreparedStatementCreator, SqlPr /** * Constructor. * - * @param filter - * the filter + * @param userId + * the user ID */ public SelectUserSettingsEntity(Long userId) { super(); diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/CloudDatumStreamSettingsEntity.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/CloudDatumStreamSettingsEntity.java index 03684c43e..75a7278f2 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/CloudDatumStreamSettingsEntity.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/domain/CloudDatumStreamSettingsEntity.java @@ -50,10 +50,8 @@ public class CloudDatumStreamSettingsEntity /** * Constructor. * - * @param userId - * the user ID - * @param dataSourceId - * the data source ID + * @param id + * the primary key * @param created * the creation date * @throws IllegalArgumentException diff --git a/solarnet/common/src/main/java/net/solarnetwork/central/security/ClientAccessTokenEntity.java b/solarnet/common/src/main/java/net/solarnetwork/central/security/ClientAccessTokenEntity.java index 374194ff9..f7312b8cb 100644 --- a/solarnet/common/src/main/java/net/solarnetwork/central/security/ClientAccessTokenEntity.java +++ b/solarnet/common/src/main/java/net/solarnetwork/central/security/ClientAccessTokenEntity.java @@ -76,7 +76,7 @@ public ClientAccessTokenEntity(UserStringStringCompositePK id, Instant created) * the user ID * @param registrationId * the registration ID - * @param principaln + * @param principalName * name the principal name * @param created * the creation date diff --git a/solarnet/oscp/src/main/java/net/solarnetwork/central/oscp/http/ExternalSystemClientRegistrationRepository.java b/solarnet/oscp/src/main/java/net/solarnetwork/central/oscp/http/ExternalSystemClientRegistrationRepository.java index 4d07b6770..af6c39870 100644 --- a/solarnet/oscp/src/main/java/net/solarnetwork/central/oscp/http/ExternalSystemClientRegistrationRepository.java +++ b/solarnet/oscp/src/main/java/net/solarnetwork/central/oscp/http/ExternalSystemClientRegistrationRepository.java @@ -58,8 +58,6 @@ public class ExternalSystemClientRegistrationRepository implements ClientRegistr /** * Constructor. * - * @param cache - * the cache * @param systemSupportDao * the system support DAO * @throws IllegalArgumentException From 7b805885251098f086a296ad5248551b23f73157 Mon Sep 17 00:00:00 2001 From: Matt Magoffin Date: Tue, 29 Oct 2024 10:50:30 +1300 Subject: [PATCH 07/11] NET-415: integrate user-level service tracking for cloud integration response length. --- .../c2c/biz/CloudIntegrationService.java | 9 +- ...eCloudIntegrationsIdentifiableService.java | 27 +- ...RestOperationsCloudDatumStreamService.java | 9 +- ...RestOperationsCloudIntegrationService.java | 9 +- .../central/c2c/config/EgaugeConfig.java | 9 +- .../central/c2c/config/LocusEnergyConfig.java | 10 +- .../central/c2c/config/SolarEdgeConfig.java | 9 +- .../central/c2c/config/SolrenViewConfig.java | 10 +- .../c2c/http/RestOperationsHelper.java | 33 +- .../config/JdbcNodeServiceAuditorConfig.java | 66 +-- .../config/JdbcUserServiceAuditorConfig.java | 90 ++++ .../common/config/ServiceAuditorSettings.java | 121 +++++ .../config/SolarNetCommonConfiguration.java | 3 + .../jdbc/BaseJdbcDatumIdServiceAuditor.java | 454 ++++++++++++++++++ .../dao/jdbc/JdbcNodeServiceAuditor.java | 372 +------------- .../dao/jdbc/JdbcUserServiceAuditor.java | 25 +- .../central/jobs/config/HttpClientConfig.java | 2 +- .../central/reg/config/HttpClientConfig.java | 15 +- 18 files changed, 834 insertions(+), 439 deletions(-) create mode 100644 solarnet/common/src/main/java/net/solarnetwork/central/common/config/JdbcUserServiceAuditorConfig.java create mode 100644 solarnet/common/src/main/java/net/solarnetwork/central/common/config/ServiceAuditorSettings.java create mode 100644 solarnet/common/src/main/java/net/solarnetwork/central/common/dao/jdbc/BaseJdbcDatumIdServiceAuditor.java diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/CloudIntegrationService.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/CloudIntegrationService.java index 82dfa7895..c39550a1b 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/CloudIntegrationService.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/CloudIntegrationService.java @@ -35,7 +35,7 @@ * API for a cloud integration service. * * @author matt - * @version 1.1 + * @version 1.2 */ public interface CloudIntegrationService extends Identity, SettingSpecifierProvider, LocalizedServiceInfoProvider { @@ -65,6 +65,13 @@ public interface CloudIntegrationService */ String BASE_URL_SETTING = "baseUrl"; + /** + * The audit service name for content processed (bytes). + * + * @since 1.2 + */ + String CONTENT_PROCESSED_AUDIT_SERVICE = "ccio"; + /** * Get a mapping of "well known" service URIs. * diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/BaseCloudIntegrationsIdentifiableService.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/BaseCloudIntegrationsIdentifiableService.java index d3581ce40..7fa04a60a 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/BaseCloudIntegrationsIdentifiableService.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/BaseCloudIntegrationsIdentifiableService.java @@ -26,6 +26,7 @@ import java.util.List; import org.springframework.security.crypto.encrypt.TextEncryptor; import net.solarnetwork.central.biz.UserEventAppenderBiz; +import net.solarnetwork.central.biz.UserServiceAuditor; import net.solarnetwork.central.c2c.domain.CloudIntegrationsUserEvents; import net.solarnetwork.settings.SettingSpecifier; import net.solarnetwork.settings.support.BaseSettingsSpecifierLocalizedServiceInfoProvider; @@ -35,7 +36,7 @@ * cloud integration services. * * @author matt - * @version 1.0 + * @version 1.1 */ public abstract class BaseCloudIntegrationsIdentifiableService extends BaseSettingsSpecifierLocalizedServiceInfoProvider @@ -53,6 +54,9 @@ public abstract class BaseCloudIntegrationsIdentifiableService /** The service settings. */ protected final List settings; + /** A user service auditor. */ + protected UserServiceAuditor userServiceAuditor; + /** * Constructor. * @@ -89,4 +93,25 @@ public final List getSettingSpecifiers() { return settings; } + /** + * Get the user service auditor. + * + * @return the auditor, or {@literal null} + * @since 1.1 + */ + public UserServiceAuditor getUserServiceAuditor() { + return userServiceAuditor; + } + + /** + * Set the user service auditor. + * + * @param userServiceAuditor + * the auditor to set, or {@literal null} + * @since 1.1 + */ + public void setUserServiceAuditor(UserServiceAuditor userServiceAuditor) { + this.userServiceAuditor = userServiceAuditor; + } + } diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/BaseRestOperationsCloudDatumStreamService.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/BaseRestOperationsCloudDatumStreamService.java index 73ea2bfcb..6c47624d4 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/BaseRestOperationsCloudDatumStreamService.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/BaseRestOperationsCloudDatumStreamService.java @@ -27,6 +27,7 @@ import org.springframework.security.crypto.encrypt.TextEncryptor; import org.springframework.web.client.RestOperations; import net.solarnetwork.central.biz.UserEventAppenderBiz; +import net.solarnetwork.central.biz.UserServiceAuditor; import net.solarnetwork.central.c2c.biz.CloudIntegrationsExpressionService; import net.solarnetwork.central.c2c.dao.CloudDatumStreamConfigurationDao; import net.solarnetwork.central.c2c.dao.CloudDatumStreamMappingConfigurationDao; @@ -41,7 +42,7 @@ * {@link RestOperations} support. * * @author matt - * @version 1.1 + * @version 1.2 */ public abstract class BaseRestOperationsCloudDatumStreamService extends BaseCloudDatumStreamService { @@ -89,4 +90,10 @@ public BaseRestOperationsCloudDatumStreamService(String serviceIdentifier, Strin this.restOpsHelper = requireNonNullArgument(restOpsHelper, "restOpsHelper"); } + @Override + public void setUserServiceAuditor(UserServiceAuditor userServiceAuditor) { + super.setUserServiceAuditor(userServiceAuditor); + restOpsHelper.setUserServiceAuditor(userServiceAuditor); + } + } diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/BaseRestOperationsCloudIntegrationService.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/BaseRestOperationsCloudIntegrationService.java index 65c670ce7..207635c5f 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/BaseRestOperationsCloudIntegrationService.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/biz/impl/BaseRestOperationsCloudIntegrationService.java @@ -30,6 +30,7 @@ import org.springframework.security.crypto.encrypt.TextEncryptor; import org.springframework.web.client.RestOperations; import net.solarnetwork.central.biz.UserEventAppenderBiz; +import net.solarnetwork.central.biz.UserServiceAuditor; import net.solarnetwork.central.c2c.biz.CloudDatumStreamService; import net.solarnetwork.central.c2c.http.RestOperationsHelper; import net.solarnetwork.settings.SettingSpecifier; @@ -40,7 +41,7 @@ * {@link RestOperations} support. * * @author matt - * @version 1.0 + * @version 1.1 */ public abstract class BaseRestOperationsCloudIntegrationService extends BaseCloudIntegrationService { @@ -79,4 +80,10 @@ public BaseRestOperationsCloudIntegrationService(String serviceIdentifier, Strin this.restOpsHelper = requireNonNullArgument(restOpsHelper, "restOpsHelper"); } + @Override + public void setUserServiceAuditor(UserServiceAuditor userServiceAuditor) { + super.setUserServiceAuditor(userServiceAuditor); + restOpsHelper.setUserServiceAuditor(userServiceAuditor); + } + } diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/EgaugeConfig.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/EgaugeConfig.java index 4d393fab2..35900c14c 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/EgaugeConfig.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/EgaugeConfig.java @@ -42,6 +42,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.web.client.RestOperations; import net.solarnetwork.central.biz.UserEventAppenderBiz; +import net.solarnetwork.central.biz.UserServiceAuditor; import net.solarnetwork.central.c2c.biz.CloudDatumStreamService; import net.solarnetwork.central.c2c.biz.CloudIntegrationService; import net.solarnetwork.central.c2c.biz.CloudIntegrationsExpressionService; @@ -61,7 +62,7 @@ * Configuration for the eGauge cloud integration services. * * @author matt - * @version 1.0 + * @version 1.1 */ @Configuration(proxyBeanMethods = false) @Profile(CLOUD_INTEGRATIONS) @@ -111,6 +112,9 @@ public class EgaugeConfig { @Autowired private CacheManager cacheManager; + @Autowired(required = false) + private UserServiceAuditor userServiceAuditor; + @Bean @Qualifier(EGAUGE_DEVICE_REGISTERS) @ConfigurationProperties(prefix = "app.c2c.cache.egague-device-registers") @@ -151,6 +155,7 @@ public ClientRegistration findByRegistrationId(String registrationId) { BaseCloudIntegrationService.class.getName()); service.setMessageSource(msgSource); + service.setUserServiceAuditor(userServiceAuditor); service.setDeviceRegistersCache(deviceRegistersCache); return service; @@ -168,6 +173,8 @@ public CloudIntegrationService egaugeCloudIntegrationService( BaseCloudIntegrationService.class.getName()); service.setMessageSource(msgSource); + service.setUserServiceAuditor(userServiceAuditor); + return service; } diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/LocusEnergyConfig.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/LocusEnergyConfig.java index c2341c1f6..33fff541b 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/LocusEnergyConfig.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/LocusEnergyConfig.java @@ -54,6 +54,7 @@ import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; import org.springframework.web.client.RestOperations; import net.solarnetwork.central.biz.UserEventAppenderBiz; +import net.solarnetwork.central.biz.UserServiceAuditor; import net.solarnetwork.central.c2c.biz.CloudDatumStreamService; import net.solarnetwork.central.c2c.biz.CloudIntegrationService; import net.solarnetwork.central.c2c.biz.CloudIntegrationsExpressionService; @@ -75,7 +76,7 @@ * Configuration for the Locus Energy cloud integration services. * * @author matt - * @version 1.1 + * @version 1.2 */ @Configuration(proxyBeanMethods = false) @Profile(CLOUD_INTEGRATIONS) @@ -122,6 +123,9 @@ public class LocusEnergyConfig { @Autowired private AsyncTaskExecutor taskExecutor; + @Autowired(required = false) + private UserServiceAuditor userServiceAuditor; + @Bean @Qualifier(LOCUS_ENERGY) public OAuth2AuthorizedClientManager oauthAuthorizedClientManager( @@ -182,6 +186,8 @@ public CloudDatumStreamService locusEnergyCloudDatumStreamService( BaseCloudDatumStreamService.class.getName()); service.setMessageSource(msgSource); + service.setUserServiceAuditor(userServiceAuditor); + return service; } @@ -198,6 +204,8 @@ public CloudIntegrationService locusEnergyCloudIntegrationService( BaseCloudIntegrationService.class.getName()); service.setMessageSource(msgSource); + service.setUserServiceAuditor(userServiceAuditor); + return service; } diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/SolarEdgeConfig.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/SolarEdgeConfig.java index e02d1db3e..12bc76fc0 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/SolarEdgeConfig.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/SolarEdgeConfig.java @@ -38,6 +38,7 @@ import org.springframework.security.crypto.encrypt.TextEncryptor; import org.springframework.web.client.RestOperations; import net.solarnetwork.central.biz.UserEventAppenderBiz; +import net.solarnetwork.central.biz.UserServiceAuditor; import net.solarnetwork.central.c2c.biz.CloudDatumStreamService; import net.solarnetwork.central.c2c.biz.CloudIntegrationService; import net.solarnetwork.central.c2c.biz.CloudIntegrationsExpressionService; @@ -56,7 +57,7 @@ * Configuration for the SolarEdge cloud integration services. * * @author matt - * @version 1.1 + * @version 1.2 */ @Configuration(proxyBeanMethods = false) @Profile(CLOUD_INTEGRATIONS) @@ -99,6 +100,9 @@ public class SolarEdgeConfig { @Autowired private CacheManager cacheManager; + @Autowired(required = false) + private UserServiceAuditor userServiceAuditor; + @Bean @Qualifier(SOLAREDGE_SITE_TZ) @ConfigurationProperties(prefix = "app.c2c.cache.solaredge-site-tz") @@ -144,6 +148,7 @@ public CloudDatumStreamService solarEdgeV1CloudDatumStreamService( BaseCloudDatumStreamService.class.getName()); service.setMessageSource(msgSource); + service.setUserServiceAuditor(userServiceAuditor); service.setSiteTimeZoneCache(solarEdgeSiteTimeZoneCache); service.setSiteInventoryCache(solarEdgeSiteInventoryCache); @@ -162,6 +167,8 @@ public CloudIntegrationService solarEdgeV1CloudIntegrationService( BaseCloudIntegrationService.class.getName()); service.setMessageSource(msgSource); + service.setUserServiceAuditor(userServiceAuditor); + return service; } diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/SolrenViewConfig.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/SolrenViewConfig.java index aa31070f8..f8fd827af 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/SolrenViewConfig.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/SolrenViewConfig.java @@ -34,6 +34,7 @@ import org.springframework.security.crypto.encrypt.TextEncryptor; import org.springframework.web.client.RestOperations; import net.solarnetwork.central.biz.UserEventAppenderBiz; +import net.solarnetwork.central.biz.UserServiceAuditor; import net.solarnetwork.central.c2c.biz.CloudDatumStreamService; import net.solarnetwork.central.c2c.biz.CloudIntegrationService; import net.solarnetwork.central.c2c.biz.CloudIntegrationsExpressionService; @@ -50,7 +51,7 @@ * Configuration for the SolrevView cloud integration services. * * @author matt - * @version 1.1 + * @version 1.2 */ @Configuration(proxyBeanMethods = false) @Profile(CLOUD_INTEGRATIONS) @@ -84,6 +85,9 @@ public class SolrenViewConfig { @Autowired private CloudIntegrationsExpressionService expressionService; + @Autowired(required = false) + private UserServiceAuditor userServiceAuditor; + @Bean @Qualifier(SOLRENVIEW) public CloudDatumStreamService solrenViewCloudDatumStreamService() { @@ -97,6 +101,8 @@ public CloudDatumStreamService solrenViewCloudDatumStreamService() { BaseCloudDatumStreamService.class.getName()); service.setMessageSource(msgSource); + service.setUserServiceAuditor(userServiceAuditor); + return service; } @@ -112,6 +118,8 @@ public CloudIntegrationService solrenViewCloudIntegrationService( BaseCloudIntegrationService.class.getName()); service.setMessageSource(msgSource); + service.setUserServiceAuditor(userServiceAuditor); + return service; } diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/http/RestOperationsHelper.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/http/RestOperationsHelper.java index 28dfec14a..8da9bcc25 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/http/RestOperationsHelper.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/http/RestOperationsHelper.java @@ -23,6 +23,7 @@ package net.solarnetwork.central.c2c.http; import static java.lang.String.format; +import static net.solarnetwork.central.c2c.biz.CloudIntegrationService.CONTENT_PROCESSED_AUDIT_SERVICE; import static net.solarnetwork.central.c2c.domain.CloudIntegrationsUserEvents.eventForConfiguration; import static net.solarnetwork.util.ObjectUtils.requireNonNullArgument; import java.net.URI; @@ -42,6 +43,7 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.client.UnknownContentTypeException; import net.solarnetwork.central.biz.UserEventAppenderBiz; +import net.solarnetwork.central.biz.UserServiceAuditor; import net.solarnetwork.central.c2c.domain.CloudIntegrationsConfigurationEntity; import net.solarnetwork.central.c2c.domain.CloudIntegrationsUserEvents; import net.solarnetwork.central.domain.UserRelatedCompositeKey; @@ -52,7 +54,7 @@ * Helper for HTTP interactions using {@link RestOperations}. * * @author matt - * @version 1.2 + * @version 1.3 */ public class RestOperationsHelper implements CloudIntegrationsUserEvents { @@ -74,8 +76,12 @@ public class RestOperationsHelper implements CloudIntegrationsUserEvents { /** The sensitive key provider. */ protected final Function> sensitiveKeyProvider; + /** A thread-local response body length tracker. */ protected final ThreadLocal responseLengthTracker; + /** An optional user service auditor, for response body counts. */ + protected UserServiceAuditor userServiceAuditor; + /** * Constructor. * @@ -246,6 +252,10 @@ public , K extends Us log.info("[{}] for {} {} tracked {} response body length: {}", description, configuration.getClass().getSimpleName(), configuration.getId().ident(), uri, len); + if ( userServiceAuditor != null ) { + userServiceAuditor.auditUserService(configuration.getUserId(), + CONTENT_PROCESSED_AUDIT_SERVICE, (int) len); + } } } } @@ -259,4 +269,25 @@ public final RestOperations getRestOps() { return restOps; } + /** + * Get the user service auditor. + * + * @return the auditor, or {@literal null} + * @since 1.3 + */ + public final UserServiceAuditor getUserServiceAuditor() { + return userServiceAuditor; + } + + /** + * Set the user service auditor. + * + * @param userServiceAuditor + * the auditor to set, or {@literal null} + * @since 1.3 + */ + public final void setUserServiceAuditor(UserServiceAuditor userServiceAuditor) { + this.userServiceAuditor = userServiceAuditor; + } + } diff --git a/solarnet/common/src/main/java/net/solarnetwork/central/common/config/JdbcNodeServiceAuditorConfig.java b/solarnet/common/src/main/java/net/solarnetwork/central/common/config/JdbcNodeServiceAuditorConfig.java index bf4d4628e..c6f93aecc 100644 --- a/solarnet/common/src/main/java/net/solarnetwork/central/common/config/JdbcNodeServiceAuditorConfig.java +++ b/solarnet/common/src/main/java/net/solarnetwork/central/common/config/JdbcNodeServiceAuditorConfig.java @@ -22,6 +22,7 @@ package net.solarnetwork.central.common.config; +import static net.solarnetwork.central.common.config.SolarNetCommonConfiguration.AUDIT; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -35,55 +36,14 @@ * Node service auditor configuration. * * @author matt - * @version 1.0 + * @version 1.1 */ @Configuration(proxyBeanMethods = false) -@Profile("node-service-auditor") +@Profile(JdbcNodeServiceAuditorConfig.NODE_SERVICE_AUDITOR) public class JdbcNodeServiceAuditorConfig { - /** A qualifier for audit JDBC access. */ - public static final String AUDIT = "audit"; - - public static class NodeServiceAuditorSettings { - - private long updateDelay = 100; - private long flushDelay = 10000; - private long connectionRecoveryDelay = 15000; - private int statLogUpdateCount = 1000; - - public long getUpdateDelay() { - return updateDelay; - } - - public void setUpdateDelay(long updateDelay) { - this.updateDelay = updateDelay; - } - - public long getFlushDelay() { - return flushDelay; - } - - public void setFlushDelay(long flushDelay) { - this.flushDelay = flushDelay; - } - - public long getConnectionRecoveryDelay() { - return connectionRecoveryDelay; - } - - public void setConnectionRecoveryDelay(long connectionRecoveryDelay) { - this.connectionRecoveryDelay = connectionRecoveryDelay; - } - - public int getStatLogUpdateCount() { - return statLogUpdateCount; - } - - public void setStatLogUpdateCount(int statLogUpdateCount) { - this.statLogUpdateCount = statLogUpdateCount; - } - - } + /** A qualifier for node service auditor. */ + public static final String NODE_SERVICE_AUDITOR = "node-service-auditor"; @Autowired private DataSource dataSource; @@ -101,10 +61,11 @@ public void setStatLogUpdateCount(int statLogUpdateCount) { * * @return the settings */ + @Qualifier(NODE_SERVICE_AUDITOR) @Bean @ConfigurationProperties(prefix = "app.node-service-auditor") - public NodeServiceAuditorSettings nodeServiceAuditorSettings() { - return new NodeServiceAuditorSettings(); + public ServiceAuditorSettings nodeServiceAuditorSettings() { + return new ServiceAuditorSettings(); } /** @@ -115,13 +76,14 @@ public NodeServiceAuditorSettings nodeServiceAuditorSettings() { * @return the service */ @Bean(initMethod = "serviceDidStartup", destroyMethod = "serviceDidShutdown") - public JdbcNodeServiceAuditor nodeServiceAuditor(NodeServiceAuditorSettings settings) { + public JdbcNodeServiceAuditor nodeServiceAuditor( + @Qualifier(NODE_SERVICE_AUDITOR) ServiceAuditorSettings settings) { DataSource ds = (readWriteDataSource != null ? readWriteDataSource : dataSource); JdbcNodeServiceAuditor auditor = new JdbcNodeServiceAuditor(ds); - auditor.setUpdateDelay(settings.updateDelay); - auditor.setFlushDelay(settings.flushDelay); - auditor.setConnectionRecoveryDelay(settings.connectionRecoveryDelay); - auditor.setStatLogUpdateCount(settings.statLogUpdateCount); + auditor.setUpdateDelay(settings.getUpdateDelay()); + auditor.setFlushDelay(settings.getFlushDelay()); + auditor.setConnectionRecoveryDelay(settings.getConnectionRecoveryDelay()); + auditor.setStatLogUpdateCount(settings.getStatLogUpdateCount()); return auditor; } diff --git a/solarnet/common/src/main/java/net/solarnetwork/central/common/config/JdbcUserServiceAuditorConfig.java b/solarnet/common/src/main/java/net/solarnetwork/central/common/config/JdbcUserServiceAuditorConfig.java new file mode 100644 index 000000000..d2c6ae7ce --- /dev/null +++ b/solarnet/common/src/main/java/net/solarnetwork/central/common/config/JdbcUserServiceAuditorConfig.java @@ -0,0 +1,90 @@ +/* ================================================================== + * JdbcUserServiceAuditorConfig - 29/10/2024 9:05:51 am + * + * Copyright 2023 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.common.config; + +import static net.solarnetwork.central.common.config.SolarNetCommonConfiguration.AUDIT; +import javax.sql.DataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import net.solarnetwork.central.common.dao.jdbc.JdbcUserServiceAuditor; + +/** + * User service auditor configuration. + * + * @author matt + * @version 1.0 + */ +@Configuration(proxyBeanMethods = false) +@Profile(JdbcUserServiceAuditorConfig.USER_SERVICE_AUDITOR) +public class JdbcUserServiceAuditorConfig { + + /** A qualifier for user service auditor. */ + public static final String USER_SERVICE_AUDITOR = "user-service-auditor"; + + @Autowired + private DataSource dataSource; + + /** + * A read-write non-primary data source, for use in apps where the primary + * data source is read-only. + */ + @Autowired(required = false) + @Qualifier(AUDIT) + private DataSource readWriteDataSource; + + /** + * Configuration settings for the user service auditor. + * + * @return the settings + */ + @Qualifier(USER_SERVICE_AUDITOR) + @Bean + @ConfigurationProperties(prefix = "app.user-service-auditor") + public ServiceAuditorSettings userServiceAuditorSettings() { + return new ServiceAuditorSettings(); + } + + /** + * Auditor for user service events. + * + * @param settings + * the settings + * @return the service + */ + @Bean(initMethod = "serviceDidStartup", destroyMethod = "serviceDidShutdown") + public JdbcUserServiceAuditor userServiceAuditor( + @Qualifier(USER_SERVICE_AUDITOR) ServiceAuditorSettings settings) { + DataSource ds = (readWriteDataSource != null ? readWriteDataSource : dataSource); + JdbcUserServiceAuditor auditor = new JdbcUserServiceAuditor(ds); + auditor.setUpdateDelay(settings.getUpdateDelay()); + auditor.setFlushDelay(settings.getFlushDelay()); + auditor.setConnectionRecoveryDelay(settings.getConnectionRecoveryDelay()); + auditor.setStatLogUpdateCount(settings.getStatLogUpdateCount()); + return auditor; + } + +} diff --git a/solarnet/common/src/main/java/net/solarnetwork/central/common/config/ServiceAuditorSettings.java b/solarnet/common/src/main/java/net/solarnetwork/central/common/config/ServiceAuditorSettings.java new file mode 100644 index 000000000..cf4f82244 --- /dev/null +++ b/solarnet/common/src/main/java/net/solarnetwork/central/common/config/ServiceAuditorSettings.java @@ -0,0 +1,121 @@ +/* ================================================================== + * ServiceAuditorSettings.java - 29/10/2024 9:53:27 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.common.config; + +/** + * Settings for service auditors. + * + * @author matt + * @version 1.0 + */ +public class ServiceAuditorSettings { + + private long updateDelay = 100; + private long flushDelay = 10000; + private long connectionRecoveryDelay = 15000; + private int statLogUpdateCount = 1000; + + /** + * Constructor. + */ + public ServiceAuditorSettings() { + super(); + } + + /** + * Get the update delay. + * + * @return the delay, in milliseconds + */ + public long getUpdateDelay() { + return updateDelay; + } + + /** + * Set the update delay. + * + * @param updateDelay + * the delay to set, in milliseconds + */ + public void setUpdateDelay(long updateDelay) { + this.updateDelay = updateDelay; + } + + /** + * Get the flush delay. + * + * @return the delay, in milliseconds + */ + public long getFlushDelay() { + return flushDelay; + } + + /** + * Set the flush delay. + * + * @param flushDelay + * the delay to set, in milliseconds + */ + public void setFlushDelay(long flushDelay) { + this.flushDelay = flushDelay; + } + + /** + * Get the connection recovery delay. + * + * @return the delay, in milliseconds + */ + public long getConnectionRecoveryDelay() { + return connectionRecoveryDelay; + } + + /** + * Set the connection recovery delay. + * + * @param connectionRecoveryDelay + * the delay to set, in milliseconds + */ + public void setConnectionRecoveryDelay(long connectionRecoveryDelay) { + this.connectionRecoveryDelay = connectionRecoveryDelay; + } + + /** + * Get the statistics log update count. + * + * @return the log update count + */ + public int getStatLogUpdateCount() { + return statLogUpdateCount; + } + + /** + * Set the statistics log update count. + * + * @param statLogUpdateCount + * the update count to set + */ + public void setStatLogUpdateCount(int statLogUpdateCount) { + this.statLogUpdateCount = statLogUpdateCount; + } + +} diff --git a/solarnet/common/src/main/java/net/solarnetwork/central/common/config/SolarNetCommonConfiguration.java b/solarnet/common/src/main/java/net/solarnetwork/central/common/config/SolarNetCommonConfiguration.java index 1d1b691ac..bf5d58c1b 100644 --- a/solarnet/common/src/main/java/net/solarnetwork/central/common/config/SolarNetCommonConfiguration.java +++ b/solarnet/common/src/main/java/net/solarnetwork/central/common/config/SolarNetCommonConfiguration.java @@ -38,4 +38,7 @@ public class SolarNetCommonConfiguration { /** A qualifier to use for OAuth client registration. */ public static final String OAUTH_CLIENT_REGISTRATION = "oauth-client-reg"; + /** A qualifier for audit JDBC access. */ + public static final String AUDIT = "audit"; + } diff --git a/solarnet/common/src/main/java/net/solarnetwork/central/common/dao/jdbc/BaseJdbcDatumIdServiceAuditor.java b/solarnet/common/src/main/java/net/solarnetwork/central/common/dao/jdbc/BaseJdbcDatumIdServiceAuditor.java new file mode 100644 index 000000000..43f3165c3 --- /dev/null +++ b/solarnet/common/src/main/java/net/solarnetwork/central/common/dao/jdbc/BaseJdbcDatumIdServiceAuditor.java @@ -0,0 +1,454 @@ +/* ================================================================== + * BaseJdbcDatumIdServiceAuditor.java - 29/10/2024 9:59:09 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.common.dao.jdbc; + +import static net.solarnetwork.util.ObjectUtils.requireNonNullArgument; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Clock; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.sql.DataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import net.solarnetwork.domain.datum.DatumId; +import net.solarnetwork.service.PingTest; +import net.solarnetwork.service.PingTestResult; +import net.solarnetwork.service.ServiceLifecycleObserver; +import net.solarnetwork.util.StatTracker; + +/** + * Base class for {@link DatumId} related service auditors using JDBC. + * + * @author matt + * @version 1.0 + */ +public abstract class BaseJdbcDatumIdServiceAuditor implements PingTest, ServiceLifecycleObserver { + + /** + * The default value for the {@code updateDelay} property. + */ + public static final long DEFAULT_UPDATE_DELAY = 100; + + /** + * The default value for the {@code flushDelay} property. + */ + public static final long DEFAULT_FLUSH_DELAY = 10000; + + /** + * The default value for the {@code connecitonRecoveryDelay} property. + */ + public static final long DEFAULT_CONNECTION_RECOVERY_DELAY = 15000; + + /** + * A regular expression that matches if a JDBC statement is a + * {@link CallableStatement}. + */ + public static final Pattern CALLABLE_STATEMENT_REGEX = Pattern.compile("^\\{call\\s.*\\}", + Pattern.CASE_INSENSITIVE); + + /** A class-level logger. */ + protected final Logger log = LoggerFactory.getLogger(getClass()); + + /** The JDBC data source. */ + protected final DataSource dataSource; + + /** + * A temporary cache of service counters. + * + *

+ * This cache is where service updates are performed. The primary key is a + * {@link DatumId} but the actual meaning of the kind, object ID, source ID, + * and timestamp components are service dependent. For example a node-based + * service might treat the {@code objectId} and {@code sourceId} as node ID + * and datum source ID values, while a user-based service might treat the + * {@code objectId} as a user ID and the {@code sourceId} as a service name. + *

+ */ + protected final ConcurrentMap serviceCounters; + + /** + * The clock to use. + * + *

+ * A tick-based clock can be used to group updates into time-based + * "buckets". + *

+ */ + protected final Clock clock; + protected final StatTracker statCounter; + + private final String writerThreadName; + + private String serviceIncrementSql; + + private WriterThread writerThread; + private long updateDelay; + private long flushDelay; + private long connectionRecoveryDelay; + + /** + * Constructor. + * + * @param dataSource + * the JDBC DataSource + * @param serviceCounters + * the service counters map + * @param clock + * the clock to use; a tick-based clock is typical, to align updates + * to time-based "buckets" + * @throws IllegalArgumentException + * if any argument is {@literal null} + */ + public BaseJdbcDatumIdServiceAuditor(DataSource dataSource, + ConcurrentMap serviceCounters, Clock clock, + StatTracker statCounter) { + super(); + this.dataSource = requireNonNullArgument(dataSource, "dataSource"); + this.serviceCounters = requireNonNullArgument(serviceCounters, "serviceCounters"); + this.clock = requireNonNullArgument(clock, "clock"); + this.statCounter = requireNonNullArgument(statCounter, "statCounter"); + this.writerThreadName = statCounter.getDisplayName() + "Writer"; + setConnectionRecoveryDelay(DEFAULT_CONNECTION_RECOVERY_DELAY); + setFlushDelay(DEFAULT_FLUSH_DELAY); + setUpdateDelay(DEFAULT_UPDATE_DELAY); + } + + @Override + public void serviceDidStartup() { + enableWriting(); + + } + + @Override + public void serviceDidShutdown() { + disableWriting(); + } + + /** + * Add a service count. + * + * @param key + * the key of the count + * @param count + * the count to add + */ + protected void addServiceCount(DatumId key, int count) { + serviceCounters.computeIfAbsent(key, k -> new AtomicInteger(0)).addAndGet(count); + statCounter.increment(JdbcNodeServiceAuditorCount.ResultsAdded); + } + + private class WriterThread extends Thread { + + private final AtomicBoolean keepGoingWithConnection = new AtomicBoolean(true); + private final AtomicBoolean keepGoing = new AtomicBoolean(true); + private boolean started = false; + + public boolean hasStarted() { + return started; + } + + public boolean isGoing() { + return keepGoing.get(); + } + + public void reconnect() { + keepGoingWithConnection.compareAndSet(true, false); + } + + public void exit() { + keepGoing.compareAndSet(true, false); + keepGoingWithConnection.compareAndSet(true, false); + } + + @Override + public void run() { + log.info("Started JDBC audit writer thread {}", this); + statCounter.increment(JdbcNodeServiceAuditorCount.WriterThreadsStarted); + try { + while ( keepGoing.get() ) { + keepGoingWithConnection.set(true); + synchronized ( this ) { + started = true; + this.notifyAll(); + } + try { + keepGoing.compareAndSet(true, execute()); + } catch ( SQLException | RuntimeException e ) { + log.warn("Exception with auditing", e); + // sleep, then try again + try { + Thread.sleep(connectionRecoveryDelay); + } catch ( InterruptedException e2 ) { + log.info("Audit writer thread interrupted: exiting now."); + keepGoing.set(false); + } + } + } + } finally { + statCounter.increment(JdbcNodeServiceAuditorCount.WriterThreadsEnded); + } + } + + private Boolean execute() throws SQLException { + try (Connection conn = dataSource.getConnection()) { + statCounter.increment(JdbcNodeServiceAuditorCount.ConnectionsCreated); + conn.setAutoCommit(true); // we want every execution of our loop to commit immediately + PreparedStatement stmt = isCallableStatement(serviceIncrementSql) + ? conn.prepareCall(serviceIncrementSql) + : conn.prepareStatement(serviceIncrementSql); + do { + try { + if ( Thread.interrupted() ) { + throw new InterruptedException(); + } + flushServiceData(stmt); + Thread.sleep(flushDelay); + } catch ( InterruptedException e ) { + log.info("Writer thread interrupted: exiting now."); + return false; + } + } while ( keepGoingWithConnection.get() ); + return true; + } + } + + } + + private void flushServiceData(PreparedStatement stmt) throws SQLException, InterruptedException { + statCounter.increment(JdbcNodeServiceAuditorCount.CountsFlushed); + for ( Iterator> itr = serviceCounters.entrySet() + .iterator(); itr.hasNext(); ) { + Map.Entry me = itr.next(); + DatumId key = me.getKey(); + AtomicInteger counter = me.getValue(); + final int count = counter.getAndSet(0); + if ( count < 1 ) { + // clean out stale 0 valued counter + itr.remove(); + statCounter.increment(JdbcNodeServiceAuditorCount.ZeroCountsCleared); + continue; + } + try { + if ( log.isTraceEnabled() ) { + log.trace("Incrementing node {} service {} @ {} count by {}", key.getObjectId(), + key.getSourceId(), key.getTimestamp(), count); + } + stmt.setObject(1, key.getObjectId()); + stmt.setString(2, key.getSourceId()); + stmt.setTimestamp(3, Timestamp.from(key.getTimestamp())); + stmt.setInt(4, count); + stmt.execute(); + statCounter.increment(JdbcNodeServiceAuditorCount.UpdatesExecuted); + if ( updateDelay > 0 ) { + Thread.sleep(updateDelay); + } + } catch ( SQLException | InterruptedException e ) { + statCounter.increment(JdbcNodeServiceAuditorCount.UpdatesFailed); + addServiceCount(key, count); + statCounter.increment(JdbcNodeServiceAuditorCount.ResultsReadded); + throw e; + } catch ( Exception e ) { + statCounter.increment(JdbcNodeServiceAuditorCount.UpdatesFailed); + addServiceCount(key, count); + statCounter.increment(JdbcNodeServiceAuditorCount.ResultsReadded); + RuntimeException re; + if ( e instanceof RuntimeException ) { + re = (RuntimeException) e; + } else { + re = new RuntimeException("Exception flushing node source audit data", e); + } + throw re; + } + } + } + + private boolean isCallableStatement(String sql) { + Matcher m = CALLABLE_STATEMENT_REGEX.matcher(sql); + return m.matches(); + } + + /** + * Cause the writing thread to re-connect to the database with a new + * connection. + */ + public synchronized void reconnectWriter() { + if ( writerThread != null && writerThread.isGoing() ) { + writerThread.reconnect(); + } + } + + /** + * Enable writing, and wait until the writing thread is going. + */ + public synchronized void enableWriting() { + if ( writerThread == null || !writerThread.isGoing() ) { + writerThread = new WriterThread(); + writerThread.setName(writerThreadName); + synchronized ( writerThread ) { + writerThread.start(); + while ( !writerThread.hasStarted() ) { + try { + writerThread.wait(5000L); + } catch ( InterruptedException e ) { + // ignore + } + } + } + } + } + + /** + * Disable writing. + */ + public synchronized void disableWriting() { + if ( writerThread != null ) { + writerThread.exit(); + } + } + + @Override + public String getPingTestId() { + return getClass().getName(); + } + + @Override + public String getPingTestName() { + return "JDBC Query Auditor"; + } + + @Override + public long getPingTestMaximumExecutionMilliseconds() { + return 1000; + } + + @Override + public Result performPingTest() throws Exception { + final WriterThread t = this.writerThread; + boolean writerRunning = t != null && t.isAlive(); + Map statMap = statCounter.allCounts(); + if ( !writerRunning ) { + return new PingTestResult(false, + (writerThread == null ? "Writer thread missing." : "Writer thread dead."), statMap); + } + return new PingTestResult(true, "Writer thread alive.", statMap); + } + + /** + * Set the delay, in milliseconds, between flushing cached audit data. + * + * @param flushDelay + * the delay, in milliseconds; defaults to + * {@link #DEFAULT_FLUSH_DELAY} + * @throws IllegalArgumentException + * if {@code flushDelay} is < 0 + */ + public void setFlushDelay(long flushDelay) { + if ( flushDelay < 0 ) { + throw new IllegalArgumentException("flushDelay must be >= 0"); + } + this.flushDelay = flushDelay; + } + + /** + * Set the delay, in milliseconds, to wait after a JDBC connection error + * before trying to recover and connect again. + * + * @param connectionRecoveryDelay + * the delay, in milliseconds; defaults t[ + * {@link #DEFAULT_CONNECTION_RECOVERY_DELAY} + * @throws IllegalArgumentException + * if {@code connectionRecoveryDelay} is < 0 + */ + public void setConnectionRecoveryDelay(long connectionRecoveryDelay) { + if ( connectionRecoveryDelay < 0 ) { + throw new IllegalArgumentException("connectionRecoveryDelay must be >= 0"); + } + this.connectionRecoveryDelay = connectionRecoveryDelay; + } + + /** + * Set the delay, in milliseconds, to wait after executing JDBC statements + * within a loop before executing another statement. + * + * @param updateDelay + * the delay, in milliseconds; defaults t[ + * {@link #DEFAULT_UPDATE_DELAY} + * @throws IllegalArgumentException + * if {@code updateDelay} is < 0 + */ + public void setUpdateDelay(long updateDelay) { + this.updateDelay = updateDelay; + } + + /** + * The JDBC statement to execute for incrementing a count for a single + * {@code DatumId} key. + * + *

+ * The statement must accept the following parameters: + *

+ * + *
    + *
  1. long - the object ID
  2. + *
  3. string - the source ID (service name)
  4. + *
  5. timestamp - the audit date
  6. + *
  7. integer - the count to add
  8. + *
+ * + * @param sql + * the SQL statement to use + */ + public void setServiceIncrementSql(String sql) { + if ( requireNonNullArgument(sql, "sql").equals(serviceIncrementSql) ) { + return; + } + this.serviceIncrementSql = sql; + reconnectWriter(); + } + + /** + * Set the statistic log update count. + * + *

+ * Setting this to something greater than {@literal 0} will cause + * {@literal INFO} level statistic log entries to be emitted every + * {@code statLogUpdateCount} records have been updated in the database. + *

+ * + * @param statLogUpdateCount + * the update count + */ + public void setStatLogUpdateCount(int statLogUpdateCount) { + statCounter.setLogFrequency(statLogUpdateCount); + } + +} diff --git a/solarnet/common/src/main/java/net/solarnetwork/central/common/dao/jdbc/JdbcNodeServiceAuditor.java b/solarnet/common/src/main/java/net/solarnetwork/central/common/dao/jdbc/JdbcNodeServiceAuditor.java index e62c5cbc2..f8609a28e 100644 --- a/solarnet/common/src/main/java/net/solarnetwork/central/common/dao/jdbc/JdbcNodeServiceAuditor.java +++ b/solarnet/common/src/main/java/net/solarnetwork/central/common/dao/jdbc/JdbcNodeServiceAuditor.java @@ -22,30 +22,15 @@ package net.solarnetwork.central.common.dao.jdbc; -import static net.solarnetwork.util.ObjectUtils.requireNonNullArgument; -import java.sql.CallableStatement; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.sql.Timestamp; import java.time.Clock; import java.time.Duration; -import java.util.Iterator; -import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import javax.sql.DataSource; -import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.solarnetwork.central.biz.NodeServiceAuditor; import net.solarnetwork.domain.datum.DatumId; -import net.solarnetwork.service.PingTest; -import net.solarnetwork.service.PingTestResult; -import net.solarnetwork.service.ServiceLifecycleObserver; import net.solarnetwork.util.StatTracker; /** @@ -61,50 +46,13 @@ * @author matt * @version 1.1 */ -public class JdbcNodeServiceAuditor implements NodeServiceAuditor, PingTest, ServiceLifecycleObserver { - - /** - * The default value for the {@code updateDelay} property. - */ - public static final long DEFAULT_UPDATE_DELAY = 100; - - /** - * The default value for the {@code flushDelay} property. - */ - public static final long DEFAULT_FLUSH_DELAY = 10000; - - /** - * The default value for the {@code connecitonRecoveryDelay} property. - */ - public static final long DEFAULT_CONNECTION_RECOVERY_DELAY = 15000; +public class JdbcNodeServiceAuditor extends BaseJdbcDatumIdServiceAuditor implements NodeServiceAuditor { /** * The default value for the {@code nodeServiceIncrementSql} property. */ public static final String DEFAULT_NODE_SERVICE_INCREMENT_SQL = "{call solardatm.audit_increment_node_count(?,?,?,?)}"; - /** - * A regular expression that matches if a JDBC statement is a - * {@link CallableStatement}. - */ - public static final Pattern CALLABLE_STATEMENT_REGEX = Pattern.compile("^\\{call\\s.*\\}", - Pattern.CASE_INSENSITIVE); - - /** A class-level logger. */ - protected final Logger log = LoggerFactory.getLogger(getClass()); - - private final DataSource dataSource; - private final ConcurrentMap nodeServiceCounters; // DatumId used with sourceId for service - private final Clock clock; - private final StatTracker statCounter; - - private String nodeServiceIncrementSql; - - private WriterThread writerThread; - private long updateDelay; - private long flushDelay; - private long connectionRecoveryDelay; - /** * Constructor. * @@ -124,36 +72,18 @@ public JdbcNodeServiceAuditor(DataSource dataSource) { * * @param dataSource * the JDBC DataSource - * @param nodeServiceCounters - * the node source counters map + * @param serviceCounters + * the service counters map * @param clock * the clock to use * @throws IllegalArgumentException * if any argument is {@literal null} */ public JdbcNodeServiceAuditor(DataSource dataSource, - ConcurrentMap nodeServiceCounters, Clock clock, + ConcurrentMap serviceCounters, Clock clock, StatTracker statCounter) { - super(); - this.dataSource = requireNonNullArgument(dataSource, "dataSource"); - this.nodeServiceCounters = requireNonNullArgument(nodeServiceCounters, "nodeServiceCounters"); - this.clock = requireNonNullArgument(clock, "clock"); - this.statCounter = requireNonNullArgument(statCounter, "statCounter"); - setConnectionRecoveryDelay(DEFAULT_CONNECTION_RECOVERY_DELAY); - setFlushDelay(DEFAULT_FLUSH_DELAY); - setUpdateDelay(DEFAULT_UPDATE_DELAY); - setNodeServiceIncrementSql(DEFAULT_NODE_SERVICE_INCREMENT_SQL); - } - - @Override - public void serviceDidStartup() { - enableWriting(); - - } - - @Override - public void serviceDidShutdown() { - disableWriting(); + super(dataSource, serviceCounters, clock, statCounter); + setServiceIncrementSql(DEFAULT_NODE_SERVICE_INCREMENT_SQL); } @Override @@ -166,299 +96,13 @@ public void auditNodeService(Long nodeId, String service, int count) { if ( count == 0 ) { return; } - addNodeServiceCount(DatumId.nodeId(nodeId, service, clock.instant()), count); - - } - - private void addNodeServiceCount(DatumId key, int count) { - nodeServiceCounters.computeIfAbsent(key, k -> new AtomicInteger(0)).addAndGet(count); - statCounter.increment(JdbcNodeServiceAuditorCount.ResultsAdded); - } - - private class WriterThread extends Thread { - - private final AtomicBoolean keepGoingWithConnection = new AtomicBoolean(true); - private final AtomicBoolean keepGoing = new AtomicBoolean(true); - private boolean started = false; - - public boolean hasStarted() { - return started; - } - - public boolean isGoing() { - return keepGoing.get(); - } - - public void reconnect() { - keepGoingWithConnection.compareAndSet(true, false); - } - - public void exit() { - keepGoing.compareAndSet(true, false); - keepGoingWithConnection.compareAndSet(true, false); - } - - @Override - public void run() { - log.info("Started JDBC audit writer thread {}", this); - statCounter.increment(JdbcNodeServiceAuditorCount.WriterThreadsStarted); - try { - while ( keepGoing.get() ) { - keepGoingWithConnection.set(true); - synchronized ( this ) { - started = true; - this.notifyAll(); - } - try { - keepGoing.compareAndSet(true, execute()); - } catch ( SQLException | RuntimeException e ) { - log.warn("Exception with auditing", e); - // sleep, then try again - try { - Thread.sleep(connectionRecoveryDelay); - } catch ( InterruptedException e2 ) { - log.info("Audit writer thread interrupted: exiting now."); - keepGoing.set(false); - } - } - } - } finally { - statCounter.increment(JdbcNodeServiceAuditorCount.WriterThreadsEnded); - } - } - - private Boolean execute() throws SQLException { - try (Connection conn = dataSource.getConnection()) { - statCounter.increment(JdbcNodeServiceAuditorCount.ConnectionsCreated); - conn.setAutoCommit(true); // we want every execution of our loop to commit immediately - PreparedStatement stmt = isCallableStatement(nodeServiceIncrementSql) - ? conn.prepareCall(nodeServiceIncrementSql) - : conn.prepareStatement(nodeServiceIncrementSql); - do { - try { - if ( Thread.interrupted() ) { - throw new InterruptedException(); - } - flushNodeServiceData(stmt); - Thread.sleep(flushDelay); - } catch ( InterruptedException e ) { - log.info("Writer thread interrupted: exiting now."); - return false; - } - } while ( keepGoingWithConnection.get() ); - return true; - } - } - - } - - private void flushNodeServiceData(PreparedStatement stmt) throws SQLException, InterruptedException { - statCounter.increment(JdbcNodeServiceAuditorCount.CountsFlushed); - for ( Iterator> itr = nodeServiceCounters.entrySet() - .iterator(); itr.hasNext(); ) { - Map.Entry me = itr.next(); - DatumId key = me.getKey(); - AtomicInteger counter = me.getValue(); - final int count = counter.getAndSet(0); - if ( count < 1 ) { - // clean out stale 0 valued counter - itr.remove(); - statCounter.increment(JdbcNodeServiceAuditorCount.ZeroCountsCleared); - continue; - } - try { - if ( log.isTraceEnabled() ) { - log.trace("Incrementing node {} service {} @ {} count by {}", key.getObjectId(), - key.getSourceId(), key.getTimestamp(), count); - } - stmt.setObject(1, key.getObjectId()); - stmt.setString(2, key.getSourceId()); - stmt.setTimestamp(3, Timestamp.from(key.getTimestamp())); - stmt.setInt(4, count); - stmt.execute(); - statCounter.increment(JdbcNodeServiceAuditorCount.UpdatesExecuted); - if ( updateDelay > 0 ) { - Thread.sleep(updateDelay); - } - } catch ( SQLException | InterruptedException e ) { - statCounter.increment(JdbcNodeServiceAuditorCount.UpdatesFailed); - addNodeServiceCount(key, count); - statCounter.increment(JdbcNodeServiceAuditorCount.ResultsReadded); - throw e; - } catch ( Exception e ) { - statCounter.increment(JdbcNodeServiceAuditorCount.UpdatesFailed); - addNodeServiceCount(key, count); - statCounter.increment(JdbcNodeServiceAuditorCount.ResultsReadded); - RuntimeException re; - if ( e instanceof RuntimeException ) { - re = (RuntimeException) e; - } else { - re = new RuntimeException("Exception flushing node source audit data", e); - } - throw re; - } - } - } - - private boolean isCallableStatement(String sql) { - Matcher m = CALLABLE_STATEMENT_REGEX.matcher(sql); - return m.matches(); - } - - /** - * Cause the writing thread to re-connect to the database with a new - * connection. - */ - public synchronized void reconnectWriter() { - if ( writerThread != null && writerThread.isGoing() ) { - writerThread.reconnect(); - } - } - - /** - * Enable writing, and wait until the writing thread is going. - */ - public synchronized void enableWriting() { - if ( writerThread == null || !writerThread.isGoing() ) { - writerThread = new WriterThread(); - writerThread.setName("JdbcNodeServiceAuditorWriter"); - synchronized ( writerThread ) { - writerThread.start(); - while ( !writerThread.hasStarted() ) { - try { - writerThread.wait(5000L); - } catch ( InterruptedException e ) { - // ignore - } - } - } - } - } - - /** - * Disable writing. - */ - public synchronized void disableWriting() { - if ( writerThread != null ) { - writerThread.exit(); - } - } + addServiceCount(DatumId.nodeId(nodeId, service, clock.instant()), count); - @Override - public String getPingTestId() { - return getClass().getName(); } @Override public String getPingTestName() { - return "JDBC Query Auditor"; - } - - @Override - public long getPingTestMaximumExecutionMilliseconds() { - return 1000; - } - - @Override - public Result performPingTest() throws Exception { - final WriterThread t = this.writerThread; - boolean writerRunning = t != null && t.isAlive(); - Map statMap = statCounter.allCounts(); - if ( !writerRunning ) { - return new PingTestResult(false, - (writerThread == null ? "Writer thread missing." : "Writer thread dead."), statMap); - } - return new PingTestResult(true, "Writer thread alive.", statMap); - } - - /** - * Set the delay, in milliseconds, between flushing cached audit data. - * - * @param flushDelay - * the delay, in milliseconds; defaults to - * {@link #DEFAULT_FLUSH_DELAY} - * @throws IllegalArgumentException - * if {@code flushDelay} is < 0 - */ - public void setFlushDelay(long flushDelay) { - if ( flushDelay < 0 ) { - throw new IllegalArgumentException("flushDelay must be >= 0"); - } - this.flushDelay = flushDelay; - } - - /** - * Set the delay, in milliseconds, to wait after a JDBC connection error - * before trying to recover and connect again. - * - * @param connectionRecoveryDelay - * the delay, in milliseconds; defaults t[ - * {@link #DEFAULT_CONNECTION_RECOVERY_DELAY} - * @throws IllegalArgumentException - * if {@code connectionRecoveryDelay} is < 0 - */ - public void setConnectionRecoveryDelay(long connectionRecoveryDelay) { - if ( connectionRecoveryDelay < 0 ) { - throw new IllegalArgumentException("connectionRecoveryDelay must be >= 0"); - } - this.connectionRecoveryDelay = connectionRecoveryDelay; - } - - /** - * Set the delay, in milliseconds, to wait after executing JDBC statements - * within a loop before executing another statement. - * - * @param updateDelay - * the delay, in milliseconds; defaults t[ - * {@link #DEFAULT_UPDATE_DELAY} - * @throws IllegalArgumentException - * if {@code updateDelay} is < 0 - */ - public void setUpdateDelay(long updateDelay) { - this.updateDelay = updateDelay; - } - - /** - * The JDBC statement to execute for incrementing a count for a single date, - * node, and source. - * - *

- * The statement must accept the following parameters: - *

- * - *
    - *
  1. long - the node ID
  2. - *
  3. string - the service name
  4. - *
  5. timestamp - the audit date
  6. - *
  7. integer - the instruction count to add
  8. - *
- * - * @param sql - * the SQL statement to use; defaults to - * {@link #DEFAULT_NODE_SERVICE_INCREMENT_SQL} - */ - public void setNodeServiceIncrementSql(String sql) { - if ( requireNonNullArgument(sql, "sql").equals(nodeServiceIncrementSql) ) { - return; - } - this.nodeServiceIncrementSql = sql; - reconnectWriter(); - } - - /** - * Set the statistic log update count. - * - *

- * Setting this to something greater than {@literal 0} will cause - * {@literal INFO} level statistic log entries to be emitted every - * {@code statLogUpdateCount} records have been updated in the database. - *

- * - * @param statLogUpdateCount - * the update count - */ - public void setStatLogUpdateCount(int statLogUpdateCount) { - statCounter.setLogFrequency(statLogUpdateCount); + return "JDBC Node Service Auditor"; } } diff --git a/solarnet/common/src/main/java/net/solarnetwork/central/common/dao/jdbc/JdbcUserServiceAuditor.java b/solarnet/common/src/main/java/net/solarnetwork/central/common/dao/jdbc/JdbcUserServiceAuditor.java index 96a5b4b87..d6a769733 100644 --- a/solarnet/common/src/main/java/net/solarnetwork/central/common/dao/jdbc/JdbcUserServiceAuditor.java +++ b/solarnet/common/src/main/java/net/solarnetwork/central/common/dao/jdbc/JdbcUserServiceAuditor.java @@ -43,16 +43,10 @@ * potential to drop some count values if the service is restarted. *

* - *

- * The fact that this implementation extends {@link JdbcNodeServiceAuditor} is a - * convenience only. All references to "nodes" in this implementation can be - * thought of as "users". - *

- * * @author matt * @version 1.0 */ -public class JdbcUserServiceAuditor extends JdbcNodeServiceAuditor implements UserServiceAuditor { +public class JdbcUserServiceAuditor extends BaseJdbcDatumIdServiceAuditor implements UserServiceAuditor { /** * The default value for the {@code nodeServiceIncrementSql} property. @@ -89,12 +83,25 @@ public JdbcUserServiceAuditor(DataSource dataSource, ConcurrentMap userServiceCounters, Clock clock, StatTracker statCounter) { super(dataSource, userServiceCounters, clock, statCounter); - setNodeServiceIncrementSql(DEFAULT_USER_SERVICE_INCREMENT_SQL); + setServiceIncrementSql(DEFAULT_USER_SERVICE_INCREMENT_SQL); + } + + @Override + public Clock getAuditClock() { + return clock; } @Override public void auditUserService(Long userId, String service, int count) { - auditNodeService(userId, service, count); + if ( count == 0 ) { + return; + } + addServiceCount(DatumId.nodeId(userId, service, clock.instant()), count); + } + + @Override + public String getPingTestName() { + return "JDBC User Service Auditor"; } } diff --git a/solarnet/solarjobs/src/main/java/net/solarnetwork/central/jobs/config/HttpClientConfig.java b/solarnet/solarjobs/src/main/java/net/solarnetwork/central/jobs/config/HttpClientConfig.java index 40dfa4511..4322abe7f 100644 --- a/solarnet/solarjobs/src/main/java/net/solarnetwork/central/jobs/config/HttpClientConfig.java +++ b/solarnet/solarjobs/src/main/java/net/solarnetwork/central/jobs/config/HttpClientConfig.java @@ -47,7 +47,7 @@ * HTTP client configuration. * * @author matt - * @version 1.0 + * @version 1.1 */ @Configuration(proxyBeanMethods = false) public class HttpClientConfig { diff --git a/solarnet/solaruser/src/main/java/net/solarnetwork/central/reg/config/HttpClientConfig.java b/solarnet/solaruser/src/main/java/net/solarnetwork/central/reg/config/HttpClientConfig.java index 3353c6ae1..04e12dd3e 100644 --- a/solarnet/solaruser/src/main/java/net/solarnetwork/central/reg/config/HttpClientConfig.java +++ b/solarnet/solaruser/src/main/java/net/solarnetwork/central/reg/config/HttpClientConfig.java @@ -23,7 +23,8 @@ package net.solarnetwork.central.reg.config; import java.time.Duration; -import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; @@ -39,13 +40,14 @@ import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; +import net.solarnetwork.central.web.support.ContentLengthTrackingClientHttpRequestInterceptor; import net.solarnetwork.web.jakarta.support.LoggingHttpRequestInterceptor; /** * HTTP client configuration. * * @author matt - * @version 1.0 + * @version 1.1 */ @Configuration(proxyBeanMethods = false) public class HttpClientConfig { @@ -170,7 +172,10 @@ public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory( @Profile("!http-trace") @Bean public RestTemplate restTemplate(ClientHttpRequestFactory reqFactory) { - return new RestTemplate(reqFactory); + ThreadLocal tl = ThreadLocal.withInitial(AtomicLong::new); + RestTemplate ops = new RestTemplate(reqFactory); + ops.setInterceptors(List.of(new ContentLengthTrackingClientHttpRequestInterceptor(tl))); + return ops; } /** @@ -183,8 +188,10 @@ public RestTemplate restTemplate(ClientHttpRequestFactory reqFactory) { @Profile("http-trace") @Bean public RestTemplate testingRestTemplate(ClientHttpRequestFactory reqFactory) { + ThreadLocal tl = ThreadLocal.withInitial(AtomicLong::new); RestTemplate debugTemplate = new RestTemplate(new BufferingClientHttpRequestFactory(reqFactory)); - debugTemplate.setInterceptors(Arrays.asList(new LoggingHttpRequestInterceptor())); + debugTemplate.setInterceptors(List.of(new ContentLengthTrackingClientHttpRequestInterceptor(tl), + new LoggingHttpRequestInterceptor())); return debugTemplate; } From 949823afec91fbcdf6bf147a798d25115e2b263a Mon Sep 17 00:00:00 2001 From: Matt Magoffin Date: Tue, 29 Oct 2024 12:15:11 +1300 Subject: [PATCH 08/11] Add migration DDL. --- solarnet-db-setup/postgres/migrations/migrate-20241029.sql | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 solarnet-db-setup/postgres/migrations/migrate-20241029.sql diff --git a/solarnet-db-setup/postgres/migrations/migrate-20241029.sql b/solarnet-db-setup/postgres/migrations/migrate-20241029.sql new file mode 100644 index 000000000..09e45b460 --- /dev/null +++ b/solarnet-db-setup/postgres/migrations/migrate-20241029.sql @@ -0,0 +1,3 @@ +-- Run this script from the parent directory, e.g. psql -f migrations/migrate-20241029.sql + +\i updates/NET-414-cloud-integrations-flux.sql From df4d3619c655ac04e4bf15c118ef14152e2fda6b Mon Sep 17 00:00:00 2001 From: Matt Magoffin Date: Tue, 29 Oct 2024 12:27:36 +1300 Subject: [PATCH 09/11] NET-397: add DDL updates. --- .../postgres/postgres-init-din.sql | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/solarnet-db-setup/postgres/postgres-init-din.sql b/solarnet-db-setup/postgres/postgres-init-din.sql index 5b74ab515..c154c553b 100644 --- a/solarnet-db-setup/postgres/postgres-init-din.sql +++ b/solarnet-db-setup/postgres/postgres-init-din.sql @@ -460,7 +460,7 @@ CREATE TABLE solardin.cin_datum_stream_poll_task ( -- index to speed up claim task query CREATE INDEX cin_datum_stream_poll_task_exec_idx ON solardin.cin_datum_stream_poll_task - (exec_at) INCLUDE (status); + (exec_at DESC) INCLUDE (status); /************************************************************************************************** @@ -510,11 +510,12 @@ BEGIN WHERE status = CASE NEW.enabled WHEN TRUE THEN 'c' ELSE 'q' END AND user_id = NEW.user_id AND ds_id IN ( - SELECT id - FROM solardin.cin_datum_stream - WHERE user_id = NEW.user_id - AND int_id = NEW.id - AND enabled = TRUE + SELECT cds.id + FROM solardin.cin_datum_stream cds + INNER JOIN solardin.cin_datum_stream_map cdsm ON cdsm.id = cds.map_id + WHERE cds.user_id = NEW.user_id + AND cdsm.int_id = NEW.id + AND cds.enabled = TRUE ); RETURN NEW; @@ -545,10 +546,11 @@ BEGIN AND ds_id = NEW.id AND EXISTS ( SELECT 1 - FROM solardin.cin_integration - WHERE user_id = NEW.user_id - AND id = NEW.int_id - AND enabled = TRUE + FROM solardin.cin_datum_stream_map cdsm + INNER JOIN solardin.cin_integration ci ON ci.id = cdsm.int_id + WHERE ci.user_id = NEW.user_id + AND cdsm.id = NEW.map_id + AND ci.enabled = TRUE ); RETURN NEW; From c5e636810a0c5ebf2da87074c9bd9608348b2b9d Mon Sep 17 00:00:00 2001 From: Matt Magoffin Date: Tue, 29 Oct 2024 12:28:00 +1300 Subject: [PATCH 10/11] NET-414: incorporate DDL changes. --- .../postgres/postgres-init-din.sql | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/solarnet-db-setup/postgres/postgres-init-din.sql b/solarnet-db-setup/postgres/postgres-init-din.sql index c154c553b..36b34d386 100644 --- a/solarnet-db-setup/postgres/postgres-init-din.sql +++ b/solarnet-db-setup/postgres/postgres-init-din.sql @@ -304,6 +304,29 @@ CREATE TABLE solardin.inin_endpoint_auth_cred ( Cloud Integrations ============================================ */ +/** + * Cloud integration user (account) configuration. + * + * @column user_id the ID of the account owner + * @column id the ID of the configuration + * @column created the creation date + * @column modified the modification date + * @column pub_in a flag to publish datum streams to SolarIn + * @column pub_flux a flag to publish datum streams to SolarFlux + */ +CREATE TABLE solardin.cin_user_settings ( + user_id BIGINT NOT NULL, + created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + modified TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + pub_in BOOLEAN NOT NULL DEFAULT TRUE, + pub_flux BOOLEAN NOT NULL DEFAULT FALSE, + CONSTRAINT cin_user_settings_pk PRIMARY KEY (user_id), + CONSTRAINT cin_user_settings_user_fk FOREIGN KEY (user_id) + REFERENCES solaruser.user_user (id) MATCH SIMPLE + ON UPDATE NO ACTION ON DELETE CASCADE +); + + /** * Cloud integration configuration. * @@ -435,6 +458,30 @@ CREATE TABLE solardin.cin_datum_stream ( ); +/** + * Cloud datum stream settings, to override cin_user_settings. + * + * @column user_id the ID of the account owner + * @column ds_id the ID of the datum stream associated with this configuration + * @column created the creation date + * @column modified the modification date + * @column pub_in a flag to publish datum streams to SolarIn + * @column pub_flux a flag to publish datum streams to SolarFlux + */ +CREATE TABLE solardin.cin_datum_stream_settings ( + user_id BIGINT NOT NULL, + ds_id BIGINT NOT NULL, + created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + modified TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + pub_in BOOLEAN NOT NULL DEFAULT TRUE, + pub_flux BOOLEAN NOT NULL DEFAULT TRUE, + CONSTRAINT cin_datum_stream_settings_pk PRIMARY KEY (user_id, ds_id), + CONSTRAINT cin_datum_stream_settings_ds_fk FOREIGN KEY (user_id, ds_id) + REFERENCES solardin.cin_datum_stream (user_id, id) MATCH SIMPLE + ON UPDATE NO ACTION ON DELETE CASCADE +); + + /** * Cloud datum stream poll task table. * From eaecb338eb4dc2ee7f887ea575b2fb76dba2ae6b Mon Sep 17 00:00:00 2001 From: Matt Magoffin Date: Tue, 29 Oct 2024 12:33:09 +1300 Subject: [PATCH 11/11] Bump versions. --- solarnet/cloud-integrations/build.gradle | 2 +- solarnet/common-test/build.gradle | 2 +- solarnet/common/build.gradle | 2 +- solarnet/solarjobs/build.gradle | 2 +- solarnet/solaruser/build.gradle | 2 +- solarnet/user-cloud-integrations/build.gradle | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/solarnet/cloud-integrations/build.gradle b/solarnet/cloud-integrations/build.gradle index a54646e8a..72aac88d0 100644 --- a/solarnet/cloud-integrations/build.gradle +++ b/solarnet/cloud-integrations/build.gradle @@ -14,7 +14,7 @@ dependencyManagement { } description = 'SolarNet: Cloud Integrations' -version = '1.6.0' +version = '1.7.0' base { archivesName = 'solarnet-cloud-integrations' diff --git a/solarnet/common-test/build.gradle b/solarnet/common-test/build.gradle index a4aab5d86..be2c24d07 100644 --- a/solarnet/common-test/build.gradle +++ b/solarnet/common-test/build.gradle @@ -14,7 +14,7 @@ dependencyManagement { } description = 'SolarNet: Common Test' -version = '2.5.0' +version = '2.6.0' base { archivesName = 'solarnet-common-test' diff --git a/solarnet/common/build.gradle b/solarnet/common/build.gradle index 8b5a39afc..2096ad735 100644 --- a/solarnet/common/build.gradle +++ b/solarnet/common/build.gradle @@ -16,7 +16,7 @@ dependencyManagement { } description = 'SolarNet: Common' -version = '2.20.0' +version = '2.21.0' base { archivesName = 'solarnet-common' diff --git a/solarnet/solarjobs/build.gradle b/solarnet/solarjobs/build.gradle index 3db78e541..230405cda 100644 --- a/solarnet/solarjobs/build.gradle +++ b/solarnet/solarjobs/build.gradle @@ -9,7 +9,7 @@ apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' description = 'SolarJobs' -version = '2.15.0' +version = '2.16.0' base { archivesName = 'solarjobs' diff --git a/solarnet/solaruser/build.gradle b/solarnet/solaruser/build.gradle index 0c7b79514..7045713b8 100644 --- a/solarnet/solaruser/build.gradle +++ b/solarnet/solaruser/build.gradle @@ -11,7 +11,7 @@ apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' description = 'SolarUser' -version = '2.24.0' +version = '2.25.0' base { archivesName = 'solaruser' diff --git a/solarnet/user-cloud-integrations/build.gradle b/solarnet/user-cloud-integrations/build.gradle index 0c40fdf58..4cd0d39dd 100644 --- a/solarnet/user-cloud-integrations/build.gradle +++ b/solarnet/user-cloud-integrations/build.gradle @@ -14,7 +14,7 @@ dependencyManagement { } description = 'SolarNet: User Cloud Integrations' -version = '1.3.1' +version = '1.4.0' base { archivesName = 'solarnet-user-cloud-integrations'