--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+}