From 6c561c138bf13555b5211342ffe4c2b997fcdead Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Wed, 31 Aug 2016 10:53:42 +0200 Subject: [PATCH] SONAR-7675 add InternalPropertiesDao --- .../ComputeEngineContainerImplTest.java | 2 +- .../src/main/java/org/sonar/db/DaoModule.java | 2 + .../src/main/java/org/sonar/db/DbClient.java | 13 +- .../src/main/java/org/sonar/db/MyBatis.java | 6 +- .../db/property/InternalPropertiesDao.java | 113 +++++ .../db/property/InternalPropertiesMapper.java | 36 ++ .../db/property/InternalPropertyDto.java | 50 +++ .../db/property/InternalPropertiesMapper.xml | 77 ++++ .../test/java/org/sonar/db/DaoModuleTest.java | 2 +- .../property/InternalPropertiesDaoTest.java | 421 ++++++++++++++++++ 10 files changed, 716 insertions(+), 6 deletions(-) create mode 100644 sonar-db/src/main/java/org/sonar/db/property/InternalPropertiesDao.java create mode 100644 sonar-db/src/main/java/org/sonar/db/property/InternalPropertiesMapper.java create mode 100644 sonar-db/src/main/java/org/sonar/db/property/InternalPropertyDto.java create mode 100644 sonar-db/src/main/resources/org/sonar/db/property/InternalPropertiesMapper.xml create mode 100644 sonar-db/src/test/java/org/sonar/db/property/InternalPropertiesDaoTest.java diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java index d1fa5e075d2..b513dfa7ef6 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java @@ -106,7 +106,7 @@ public class ComputeEngineContainerImplTest { assertThat(picoContainer.getParent().getParent().getParent().getComponentAdapters()).hasSize( COMPONENTS_IN_LEVEL_1_AT_CONSTRUCTION + 25 // level 1 - + 48 // content of DaoModule + + 49 // content of DaoModule + 2 // content of EsSearchModule + 54 // content of CorePropertyDefinitions + 1 // content of CePropertyDefinitions diff --git a/sonar-db/src/main/java/org/sonar/db/DaoModule.java b/sonar-db/src/main/java/org/sonar/db/DaoModule.java index e17fc4911fc..880090dcdde 100644 --- a/sonar-db/src/main/java/org/sonar/db/DaoModule.java +++ b/sonar-db/src/main/java/org/sonar/db/DaoModule.java @@ -52,6 +52,7 @@ import org.sonar.db.notification.NotificationQueueDao; import org.sonar.db.permission.PermissionDao; import org.sonar.db.permission.template.PermissionTemplateCharacteristicDao; import org.sonar.db.permission.template.PermissionTemplateDao; +import org.sonar.db.property.InternalPropertiesDao; import org.sonar.db.property.PropertiesDao; import org.sonar.db.purge.PurgeDao; import org.sonar.db.qualitygate.ProjectQgateAssociationDao; @@ -89,6 +90,7 @@ public class DaoModule extends Module { FileSourceDao.class, GroupDao.class, GroupMembershipDao.class, + InternalPropertiesDao.class, IssueDao.class, IssueChangeDao.class, IssueFilterDao.class, diff --git a/sonar-db/src/main/java/org/sonar/db/DbClient.java b/sonar-db/src/main/java/org/sonar/db/DbClient.java index 48761c376d8..35cedfc02a6 100644 --- a/sonar-db/src/main/java/org/sonar/db/DbClient.java +++ b/sonar-db/src/main/java/org/sonar/db/DbClient.java @@ -25,12 +25,13 @@ import javax.annotation.Nullable; import org.sonar.db.activity.ActivityDao; import org.sonar.db.ce.CeActivityDao; import org.sonar.db.ce.CeQueueDao; +import org.sonar.db.ce.CeScannerContextDao; import org.sonar.db.ce.CeTaskInputDao; import org.sonar.db.component.ComponentDao; +import org.sonar.db.component.ComponentKeyUpdaterDao; import org.sonar.db.component.ComponentLinkDao; import org.sonar.db.component.ResourceDao; import org.sonar.db.component.ResourceIndexDao; -import org.sonar.db.component.ComponentKeyUpdaterDao; import org.sonar.db.component.SnapshotDao; import org.sonar.db.dashboard.ActiveDashboardDao; import org.sonar.db.dashboard.DashboardDao; @@ -50,8 +51,9 @@ import org.sonar.db.measure.custom.CustomMeasureDao; import org.sonar.db.metric.MetricDao; import org.sonar.db.notification.NotificationQueueDao; import org.sonar.db.permission.PermissionDao; -import org.sonar.db.permission.template.PermissionTemplateDao; import org.sonar.db.permission.template.PermissionTemplateCharacteristicDao; +import org.sonar.db.permission.template.PermissionTemplateDao; +import org.sonar.db.property.InternalPropertiesDao; import org.sonar.db.property.PropertiesDao; import org.sonar.db.purge.PurgeDao; import org.sonar.db.qualitygate.ProjectQgateAssociationDao; @@ -60,7 +62,6 @@ import org.sonar.db.qualitygate.QualityGateDao; import org.sonar.db.qualityprofile.ActiveRuleDao; import org.sonar.db.qualityprofile.QualityProfileDao; import org.sonar.db.rule.RuleDao; -import org.sonar.db.ce.CeScannerContextDao; import org.sonar.db.source.FileSourceDao; import org.sonar.db.user.AuthorDao; import org.sonar.db.user.AuthorizationDao; @@ -78,6 +79,7 @@ public class DbClient { private final QualityProfileDao qualityProfileDao; private final LoadedTemplateDao loadedTemplateDao; private final PropertiesDao propertiesDao; + private final InternalPropertiesDao internalPropertiesDao; private final SnapshotDao snapshotDao; private final ComponentDao componentDao; private final ResourceDao resourceDao; @@ -135,6 +137,7 @@ public class DbClient { qualityProfileDao = getDao(map, QualityProfileDao.class); loadedTemplateDao = getDao(map, LoadedTemplateDao.class); propertiesDao = getDao(map, PropertiesDao.class); + internalPropertiesDao = getDao(map, InternalPropertiesDao.class); snapshotDao = getDao(map, SnapshotDao.class); componentDao = getDao(map, ComponentDao.class); resourceDao = getDao(map, ResourceDao.class); @@ -228,6 +231,10 @@ public class DbClient { return propertiesDao; } + public InternalPropertiesDao internalPropertiesDao() { + return internalPropertiesDao; + } + public SnapshotDao snapshotDao() { return snapshotDao; } diff --git a/sonar-db/src/main/java/org/sonar/db/MyBatis.java b/sonar-db/src/main/java/org/sonar/db/MyBatis.java index 9bd59be0240..c9df79927b7 100644 --- a/sonar-db/src/main/java/org/sonar/db/MyBatis.java +++ b/sonar-db/src/main/java/org/sonar/db/MyBatis.java @@ -92,6 +92,8 @@ import org.sonar.db.permission.template.PermissionTemplateDto; import org.sonar.db.permission.template.PermissionTemplateGroupDto; import org.sonar.db.permission.template.PermissionTemplateMapper; import org.sonar.db.permission.template.PermissionTemplateUserDto; +import org.sonar.db.property.InternalPropertiesMapper; +import org.sonar.db.property.InternalPropertyDto; import org.sonar.db.property.PropertiesMapper; import org.sonar.db.property.PropertyDto; import org.sonar.db.purge.IdUuidPair; @@ -175,6 +177,7 @@ public class MyBatis { confBuilder.loadAlias("MeasureFilterFavourite", MeasureFilterFavouriteDto.class); confBuilder.loadAlias("NotificationQueue", NotificationQueueDto.class); confBuilder.loadAlias("Property", PropertyDto.class); + confBuilder.loadAlias("InternalProperty", InternalPropertyDto.class); confBuilder.loadAlias("PurgeableAnalysis", PurgeableAnalysisDto.class); confBuilder.loadAlias("QualityGate", QualityGateDto.class); confBuilder.loadAlias("QualityGateCondition", QualityGateConditionDto.class); @@ -227,7 +230,8 @@ public class MyBatis { IsAliveMapper.class, LoadedTemplateMapper.class, MeasureFilterMapper.class, MeasureFilterFavouriteMapper.class, PermissionTemplateMapper.class, PermissionTemplateCharacteristicMapper.class, - PropertiesMapper.class, PurgeMapper.class, ComponentKeyUpdaterMapper.class, ResourceIndexMapper.class, RoleMapper.class, RuleMapper.class, + PropertiesMapper.class, InternalPropertiesMapper.class, + PurgeMapper.class, ComponentKeyUpdaterMapper.class, ResourceIndexMapper.class, RoleMapper.class, RuleMapper.class, SchemaMigrationMapper.class, WidgetMapper.class, WidgetPropertyMapper.class, UserMapper.class, GroupMapper.class, UserGroupMapper.class, UserTokenMapper.class, FileSourceMapper.class, diff --git a/sonar-db/src/main/java/org/sonar/db/property/InternalPropertiesDao.java b/sonar-db/src/main/java/org/sonar/db/property/InternalPropertiesDao.java new file mode 100644 index 00000000000..b99d6ed3200 --- /dev/null +++ b/sonar-db/src/main/java/org/sonar/db/property/InternalPropertiesDao.java @@ -0,0 +1,113 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db.property; + +import java.util.Optional; +import javax.annotation.Nullable; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.log.Loggers; +import org.sonar.db.Dao; +import org.sonar.db.DbSession; + +import static com.google.common.base.Preconditions.checkArgument; + +public class InternalPropertiesDao implements Dao { + + private static final int TEXT_VALUE_MAX_LENGTH = 4000; + private static final Optional OPTIONAL_OF_EMPTY_STRING = Optional.of(""); + + private final System2 system2; + + public InternalPropertiesDao(System2 system2) { + this.system2 = system2; + } + + /** + * Save a property which value is not empty. + *

Value can't be {@code null} but can have any size except 0.

+ * + * @throws IllegalArgumentException if {@code key} or {@code value} is {@code null} or empty. + * + * @see #saveAsEmpty(DbSession, String) + */ + public void save(DbSession dbSession, String key, String value) { + checkKey(key); + checkArgument(value != null && !value.isEmpty(), "value can't be null nor empty"); + + InternalPropertiesMapper mapper = getMapper(dbSession); + mapper.deleteByKey(key); + long now = system2.now(); + if (mustsBeStoredInClob(value)) { + mapper.insertAsClob(key, value, now); + } else { + mapper.insertAsText(key, value, now); + } + } + + private static boolean mustsBeStoredInClob(String value) { + return value.length() > TEXT_VALUE_MAX_LENGTH; + } + + /** + * Save a property which value is empty. + */ + public void saveAsEmpty(DbSession dbSession, String key) { + checkKey(key); + + InternalPropertiesMapper mapper = getMapper(dbSession); + mapper.deleteByKey(key); + mapper.insertAsEmpty(key, system2.now()); + } + + /** + * No streaming of value + */ + public Optional selectByKey(DbSession dbSession, String key) { + checkKey(key); + + InternalPropertiesMapper mapper = getMapper(dbSession); + InternalPropertyDto res = mapper.selectAsText(key); + if (res == null) { + return Optional.empty(); + } + if (res.isEmpty()) { + return OPTIONAL_OF_EMPTY_STRING; + } + if (res.getValue() != null) { + return Optional.of(res.getValue()); + } + res = mapper.selectAsClob(key); + if (res == null) { + Loggers.get(InternalPropertiesDao.class) + .debug("Internal property {} has been found in db but has neither text value nor is empty. " + + "Still we couldn't be retrieved with clob value. Ignoring the property.", key); + return Optional.empty(); + } + return Optional.of(res.getValue()); + } + + private static void checkKey(@Nullable String key) { + checkArgument(key != null && !key.isEmpty(), "key can't be null nor empty"); + } + + private static InternalPropertiesMapper getMapper(DbSession dbSession) { + return dbSession.getMapper(InternalPropertiesMapper.class); + } +} diff --git a/sonar-db/src/main/java/org/sonar/db/property/InternalPropertiesMapper.java b/sonar-db/src/main/java/org/sonar/db/property/InternalPropertiesMapper.java new file mode 100644 index 00000000000..8b3163b79f8 --- /dev/null +++ b/sonar-db/src/main/java/org/sonar/db/property/InternalPropertiesMapper.java @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db.property; + +import org.apache.ibatis.annotations.Param; + +public interface InternalPropertiesMapper { + InternalPropertyDto selectAsText(@Param("key") String key); + + InternalPropertyDto selectAsClob(@Param("key") String key); + + void insertAsEmpty(@Param("key") String key, @Param("createdAt") long createdAt); + + void insertAsText(@Param("key") String key, @Param("value") String value, @Param("createdAt") long createdAt); + + void insertAsClob(@Param("key") String key, @Param("value") String value, @Param("createdAt") long createdAt); + + void deleteByKey(@Param("key") String key); +} diff --git a/sonar-db/src/main/java/org/sonar/db/property/InternalPropertyDto.java b/sonar-db/src/main/java/org/sonar/db/property/InternalPropertyDto.java new file mode 100644 index 00000000000..a15d1a4a56f --- /dev/null +++ b/sonar-db/src/main/java/org/sonar/db/property/InternalPropertyDto.java @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db.property; + +public final class InternalPropertyDto { + private String key; + private boolean empty; + private String value; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public boolean isEmpty() { + return empty; + } + + public void setEmpty(boolean empty) { + this.empty = empty; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/sonar-db/src/main/resources/org/sonar/db/property/InternalPropertiesMapper.xml b/sonar-db/src/main/resources/org/sonar/db/property/InternalPropertiesMapper.xml new file mode 100644 index 00000000000..33de81df5e6 --- /dev/null +++ b/sonar-db/src/main/resources/org/sonar/db/property/InternalPropertiesMapper.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + INSERT INTO internal_properties + ( + kee, is_empty, created_at + ) + VALUES ( + #{key}, ${_true}, #{createdAt} + ) + + + + INSERT INTO internal_properties + ( + kee, + is_empty, + text_value, + created_at + ) + VALUES ( + #{key}, + ${_false}, + #{value}, + #{createdAt} + ) + + + + INSERT INTO internal_properties + ( + kee, + is_empty, + clob_value, + created_at + ) + VALUES ( + #{key}, + ${_false}, + #{value}, + #{createdAt} + ) + + + + delete from internal_properties + where + kee=#{key} + + + + diff --git a/sonar-db/src/test/java/org/sonar/db/DaoModuleTest.java b/sonar-db/src/test/java/org/sonar/db/DaoModuleTest.java index f653c76bfce..8e8d42a1996 100644 --- a/sonar-db/src/test/java/org/sonar/db/DaoModuleTest.java +++ b/sonar-db/src/test/java/org/sonar/db/DaoModuleTest.java @@ -29,6 +29,6 @@ public class DaoModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new DaoModule().configure(container); - assertThat(container.size()).isEqualTo(2 + 48); + assertThat(container.size()).isEqualTo(2 + 49); } } diff --git a/sonar-db/src/test/java/org/sonar/db/property/InternalPropertiesDaoTest.java b/sonar-db/src/test/java/org/sonar/db/property/InternalPropertiesDaoTest.java new file mode 100644 index 00000000000..ad234e3ed35 --- /dev/null +++ b/sonar-db/src/test/java/org/sonar/db/property/InternalPropertiesDaoTest.java @@ -0,0 +1,421 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db.property; + +import java.util.Map; +import java.util.Objects; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.assertj.core.api.AbstractAssert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.utils.System2; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class InternalPropertiesDaoTest { + + private static final String EMPTY_STRING = ""; + private static final String A_KEY = "a_key"; + private static final String VALUE_1 = "one"; + private static final String VALUE_2 = "two"; + private static final long DATE_1 = 1_500_000_000_000L; + private static final long DATE_2 = 1_600_000_000_000L; + private static final String VALUE_SMALL = "some small value"; + private static final String VALUE_SIZE_4000 = String.format("%1$4000.4000s", "*"); + private static final String VALUE_SIZE_4001 = VALUE_SIZE_4000 + "P"; + + private System2 system2 = mock(System2.class); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public DbTester dbTester = DbTester.create(system2); + + private DbSession dbSession = dbTester.getSession(); + + private InternalPropertiesDao underTest = new InternalPropertiesDao(system2); + + @Test + public void save_throws_IAE_if_key_is_null() { + expectKeyNullOrEmptyIAE(); + + underTest.save(dbSession, null, VALUE_SMALL); + } + + @Test + public void save_throws_IAE_if_key_is_empty() { + expectKeyNullOrEmptyIAE(); + + underTest.save(dbSession, EMPTY_STRING, VALUE_SMALL); + } + + @Test + public void save_throws_IAE_if_value_is_null() { + expectValueNullOrEmptyIAE(); + + underTest.save(dbSession, A_KEY, null); + } + + @Test + public void save_throws_IAE_if_value_is_empty() { + expectValueNullOrEmptyIAE(); + + underTest.save(dbSession, A_KEY, EMPTY_STRING); + } + + @Test + public void save_persists_value_in_varchar_if_less_than_4000() { + when(system2.now()).thenReturn(DATE_2); + underTest.save(dbSession, A_KEY, VALUE_SMALL); + + assertThatInternalProperty(A_KEY) + .hasTextValue(VALUE_SMALL) + .hasCreatedAt(DATE_2); + } + + @Test + public void save_persists_value_in_varchar_if_4000() { + when(system2.now()).thenReturn(DATE_1); + underTest.save(dbSession, A_KEY, VALUE_SIZE_4000); + + assertThatInternalProperty(A_KEY) + .hasTextValue(VALUE_SIZE_4000) + .hasCreatedAt(DATE_1); + } + + @Test + public void save_persists_value_in_varchar_if_more_than_4000() { + when(system2.now()).thenReturn(DATE_2); + + underTest.save(dbSession, A_KEY, VALUE_SIZE_4001); + + assertThatInternalProperty(A_KEY) + .hasClobValue(VALUE_SIZE_4001) + .hasCreatedAt(DATE_2); + } + + @Test + public void save_persists_new_value_in_varchar_if_4000_when_old_one_was_in_varchar() { + when(system2.now()).thenReturn(DATE_1, DATE_2); + + underTest.save(dbSession, A_KEY, VALUE_SMALL); + assertThatInternalProperty(A_KEY) + .hasTextValue(VALUE_SMALL) + .hasCreatedAt(DATE_1); + + underTest.save(dbSession, A_KEY, VALUE_SIZE_4000); + assertThatInternalProperty(A_KEY) + .hasTextValue(VALUE_SIZE_4000) + .hasCreatedAt(DATE_2); + } + + @Test + public void save_persists_new_value_in_clob_if_more_than_4000_when_old_one_was_in_varchar() { + when(system2.now()).thenReturn(DATE_1, DATE_2); + + underTest.save(dbSession, A_KEY, VALUE_SMALL); + assertThatInternalProperty(A_KEY) + .hasTextValue(VALUE_SMALL) + .hasCreatedAt(DATE_1); + + underTest.save(dbSession, A_KEY, VALUE_SIZE_4001); + assertThatInternalProperty(A_KEY) + .hasClobValue(VALUE_SIZE_4001) + .hasCreatedAt(DATE_2); + } + + @Test + public void save_persists_new_value_in_varchar_if_less_than_4000_when_old_one_was_in_clob() { + when(system2.now()).thenReturn(DATE_1, DATE_2); + + underTest.save(dbSession, A_KEY, VALUE_SIZE_4001); + assertThatInternalProperty(A_KEY) + .hasClobValue(VALUE_SIZE_4001) + .hasCreatedAt(DATE_1); + + underTest.save(dbSession, A_KEY, VALUE_SMALL); + assertThatInternalProperty(A_KEY) + .hasTextValue(VALUE_SMALL) + .hasCreatedAt(DATE_2); + } + + @Test + public void save_persists_new_value_in_clob_if_more_than_4000_when_old_one_was_in_clob() { + when(system2.now()).thenReturn(DATE_1, DATE_2); + + String oldValue = VALUE_SIZE_4001 + "blabla"; + underTest.save(dbSession, A_KEY, oldValue); + assertThatInternalProperty(A_KEY) + .hasClobValue(oldValue) + .hasCreatedAt(DATE_1); + + underTest.save(dbSession, A_KEY, VALUE_SIZE_4001); + assertThatInternalProperty(A_KEY) + .hasClobValue(VALUE_SIZE_4001) + .hasCreatedAt(DATE_2); + } + + @Test + public void saveAsEmpty_throws_IAE_if_key_is_null() { + expectKeyNullOrEmptyIAE(); + + underTest.saveAsEmpty(dbSession, null); + } + + @Test + public void saveAsEmpty_throws_IAE_if_key_is_empty() { + expectKeyNullOrEmptyIAE(); + + underTest.saveAsEmpty(dbSession, EMPTY_STRING); + } + + @Test + public void saveAsEmpty_persist_property_without_textvalue_nor_clob_value() { + when(system2.now()).thenReturn(DATE_2); + + underTest.saveAsEmpty(dbSession, A_KEY); + + assertThatInternalProperty(A_KEY) + .isEmpty() + .hasCreatedAt(DATE_2); + } + + @Test + public void saveAsEmpty_persist_property_without_textvalue_nor_clob_value_when_old_value_was_in_varchar() { + when(system2.now()).thenReturn(DATE_1, DATE_2); + + underTest.save(dbSession, A_KEY, VALUE_SMALL); + assertThatInternalProperty(A_KEY) + .hasTextValue(VALUE_SMALL) + .hasCreatedAt(DATE_1); + + underTest.saveAsEmpty(dbSession, A_KEY); + assertThatInternalProperty(A_KEY) + .isEmpty() + .hasCreatedAt(DATE_2); + } + + @Test + public void saveAsEmpty_persist_property_without_textvalue_nor_clob_value_when_old_value_was_in_clob() { + when(system2.now()).thenReturn(DATE_2, DATE_1); + + underTest.save(dbSession, A_KEY, VALUE_SIZE_4001); + assertThatInternalProperty(A_KEY) + .hasClobValue(VALUE_SIZE_4001) + .hasCreatedAt(DATE_2); + + underTest.saveAsEmpty(dbSession, A_KEY); + assertThatInternalProperty(A_KEY) + .isEmpty() + .hasCreatedAt(DATE_1); + } + + @Test + public void selectByKey_throws_IAE_when_key_is_null() { + expectKeyNullOrEmptyIAE(); + + underTest.selectByKey(dbSession, null); + } + + @Test + public void selectByKey_throws_IAE_when_key_is_empty() { + expectKeyNullOrEmptyIAE(); + + underTest.selectByKey(dbSession, EMPTY_STRING); + } + + @Test + public void selectByKey_returns_empty_optional_when_property_does_not_exist_in_DB() { + assertThat(underTest.selectByKey(dbSession, A_KEY)).isEmpty(); + } + + @Test + public void selectByKey_returns_empty_string_when_property_is_empty_in_DB() { + underTest.saveAsEmpty(dbSession, A_KEY); + + assertThat(underTest.selectByKey(dbSession, A_KEY)).contains(EMPTY_STRING); + } + + @Test + public void selectByKey_returns_value_when_property_has_value_stored_in_varchar() { + underTest.save(dbSession, A_KEY, VALUE_SMALL); + + assertThat(underTest.selectByKey(dbSession, A_KEY)).contains(VALUE_SMALL); + } + + @Test + public void selectByKey_returns_value_when_property_has_value_stored_in_clob() { + underTest.save(dbSession, A_KEY, VALUE_SIZE_4001); + + assertThat(underTest.selectByKey(dbSession, A_KEY)).contains(VALUE_SIZE_4001); + } + + private void expectKeyNullOrEmptyIAE() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("key can't be null nor empty"); + } + + private void expectValueNullOrEmptyIAE() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("value can't be null nor empty"); + } + + private InternalPropertyAssert assertThatInternalProperty(String key) { + return new InternalPropertyAssert(dbTester, dbSession, key); + } + + private static class InternalPropertyAssert extends AbstractAssert { + + private InternalPropertyAssert(DbTester dbTester, DbSession dbSession, String internalPropertyKey) { + super(asInternalProperty(dbTester, dbSession, internalPropertyKey), InternalPropertyAssert.class); + } + + private static InternalProperty asInternalProperty(DbTester dbTester, DbSession dbSession, String internalPropertyKey) { + Map row = dbTester.selectFirst( + dbSession, + "select" + + " is_empty as \"isEmpty\", text_value as \"textValue\", clob_value as \"clobValue\", created_at as \"createdAt\"" + + " from internal_properties" + + " where kee='" + internalPropertyKey + "'"); + return new InternalProperty( + isEmpty(row), + (String) row.get("textValue"), + (String) row.get("clobValue"), + (Long) row.get("createdAt")); + } + + private static Boolean isEmpty(Map row) { + Object flag = row.get("isEmpty"); + if (flag instanceof Boolean) { + return (Boolean) flag; + } + if (flag instanceof Long) { + Long longBoolean = (Long) flag; + return longBoolean.equals(1L); + } + throw new IllegalArgumentException("Unsupported object type returned for column \"isEmpty\": " + flag.getClass()); + } + + public void doesNotExist() { + isNull(); + } + + public InternalPropertyAssert isEmpty() { + isNotNull(); + + if (!Objects.equals(actual.isEmpty(), TRUE)) { + failWithMessage("Expected Internal property to have column IS_EMPTY to be <%s> but was <%s>", true, actual.isEmpty()); + } + if (actual.getTextValue() != null) { + failWithMessage("Expected Internal property to have column TEXT_VALUE to be null but was <%s>", actual.getTextValue()); + } + if (actual.getClobValue() != null) { + failWithMessage("Expected Internal property to have column CLOB_VALUE to be null but was <%s>", actual.getClobValue()); + } + + return this; + } + + public InternalPropertyAssert hasTextValue(String expected) { + isNotNull(); + + if (!Objects.equals(actual.getTextValue(), expected)) { + failWithMessage("Expected Internal property to have column TEXT_VALUE to be <%s> but was <%s>", true, actual.getTextValue()); + } + if (actual.getClobValue() != null) { + failWithMessage("Expected Internal property to have column CLOB_VALUE to be null but was <%s>", actual.getClobValue()); + } + if (!Objects.equals(actual.isEmpty(), FALSE)) { + failWithMessage("Expected Internal property to have column IS_EMPTY to be <%s> but was <%s>", false, actual.isEmpty()); + } + + return this; + } + + public InternalPropertyAssert hasClobValue(String expected) { + isNotNull(); + + if (!Objects.equals(actual.getClobValue(), expected)) { + failWithMessage("Expected Internal property to have column CLOB_VALUE to be <%s> but was <%s>", true, actual.getClobValue()); + } + if (actual.getTextValue() != null) { + failWithMessage("Expected Internal property to have column TEXT_VALUE to be null but was <%s>", actual.getTextValue()); + } + if (!Objects.equals(actual.isEmpty(), FALSE)) { + failWithMessage("Expected Internal property to have column IS_EMPTY to be <%s> but was <%s>", false, actual.isEmpty()); + } + + return this; + } + + public InternalPropertyAssert hasCreatedAt(long expected) { + isNotNull(); + + if (!Objects.equals(actual.getCreatedAt(), expected)) { + failWithMessage("Expected Internal property to have column CREATED_AT to be <%s> but was <%s>", expected, actual.getCreatedAt()); + } + + return this; + } + + } + + private static final class InternalProperty { + private final Boolean empty; + private final String textValue; + private final String clobValue; + private final Long createdAt; + + public InternalProperty(@Nullable Boolean empty, @Nullable String textValue, @Nullable String clobValue, @Nullable Long createdAt) { + this.empty = empty; + this.textValue = textValue; + this.clobValue = clobValue; + this.createdAt = createdAt; + } + + @CheckForNull + public Boolean isEmpty() { + return empty; + } + + @CheckForNull + public String getTextValue() { + return textValue; + } + + @CheckForNull + public String getClobValue() { + return clobValue; + } + + @CheckForNull + public Long getCreatedAt() { + return createdAt; + } + } +} -- 2.39.5