aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>2016-08-31 10:53:42 +0200
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>2016-09-05 11:32:17 +0200
commit6c561c138bf13555b5211342ffe4c2b997fcdead (patch)
tree03691c5ff2f1178c7dbe0dc59c36363d981c3085
parente0188c9d2755900310c98e218cd68c14dc695853 (diff)
downloadsonarqube-6c561c138bf13555b5211342ffe4c2b997fcdead.tar.gz
sonarqube-6c561c138bf13555b5211342ffe4c2b997fcdead.zip
SONAR-7675 add InternalPropertiesDao
-rw-r--r--server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java2
-rw-r--r--sonar-db/src/main/java/org/sonar/db/DaoModule.java2
-rw-r--r--sonar-db/src/main/java/org/sonar/db/DbClient.java13
-rw-r--r--sonar-db/src/main/java/org/sonar/db/MyBatis.java6
-rw-r--r--sonar-db/src/main/java/org/sonar/db/property/InternalPropertiesDao.java113
-rw-r--r--sonar-db/src/main/java/org/sonar/db/property/InternalPropertiesMapper.java36
-rw-r--r--sonar-db/src/main/java/org/sonar/db/property/InternalPropertyDto.java50
-rw-r--r--sonar-db/src/main/resources/org/sonar/db/property/InternalPropertiesMapper.xml77
-rw-r--r--sonar-db/src/test/java/org/sonar/db/DaoModuleTest.java2
-rw-r--r--sonar-db/src/test/java/org/sonar/db/property/InternalPropertiesDaoTest.java421
10 files changed, 716 insertions, 6 deletions
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<String> 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.
+ * <p>Value can't be {@code null} but can have any size except 0.</p>
+ *
+ * @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<String> 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 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="org.sonar.db.property.InternalPropertiesMapper">
+
+ <select id="selectAsText" parameterType="map" resultType="InternalProperty">
+ select
+ is_empty as empty,
+ text_value as value,
+ created_at as createdAt
+ from
+ internal_properties
+ where
+ kee = #{key}
+ </select>
+
+ <select id="selectAsClob" parameterType="map" resultType="InternalProperty">
+ select
+ is_empty as empty,
+ clob_value as value,
+ created_at as createdAt
+ from
+ internal_properties
+ where
+ kee = #{key}
+ </select>
+
+ <insert id="insertAsEmpty" parameterType="Map" useGeneratedKeys="false">
+ INSERT INTO internal_properties
+ (
+ kee, is_empty, created_at
+ )
+ VALUES (
+ #{key}, ${_true}, #{createdAt}
+ )
+ </insert>
+
+ <insert id="insertAsText" parameterType="Map" useGeneratedKeys="false">
+ INSERT INTO internal_properties
+ (
+ kee,
+ is_empty,
+ text_value,
+ created_at
+ )
+ VALUES (
+ #{key},
+ ${_false},
+ #{value},
+ #{createdAt}
+ )
+ </insert>
+
+ <insert id="insertAsClob" parameterType="Map" useGeneratedKeys="false">
+ INSERT INTO internal_properties
+ (
+ kee,
+ is_empty,
+ clob_value,
+ created_at
+ )
+ VALUES (
+ #{key},
+ ${_false},
+ #{value},
+ #{createdAt}
+ )
+ </insert>
+
+ <delete id="deleteByKey" parameterType="String">
+ delete from internal_properties
+ where
+ kee=#{key}
+ </delete>
+
+
+</mapper>
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<InternalPropertyAssert, InternalProperty> {
+
+ 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<String, Object> 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<String, Object> 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;
+ }
+ }
+}