/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info 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 com.google.common.base.Strings;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.api.resources.Scopes;
import org.sonar.api.utils.System2;
import org.sonar.db.Dao;
import org.sonar.db.DatabaseUtils;
import org.sonar.db.DbSession;
import org.sonar.db.MyBatis;
import org.sonar.db.WildcardPosition;
import static com.google.common.base.Preconditions.checkArgument;
import static org.sonar.db.DaoDatabaseUtils.buildLikeValue;
import static org.sonar.db.DatabaseUtils.executeLargeInputs;
import static org.sonar.db.DatabaseUtils.executeLargeInputsWithoutOutput;
public class PropertiesDao implements Dao {
private static final String NOTIFICATION_PREFIX = "notification.";
private static final int VARCHAR_MAXSIZE = 4000;
private final MyBatis mybatis;
private final System2 system2;
public PropertiesDao(MyBatis mybatis, System2 system2) {
this.mybatis = mybatis;
this.system2 = system2;
}
/**
* Returns the logins of users who have subscribed to the given notification dispatcher with the given notification channel.
* If a resource ID is passed, the search is made on users who have specifically subscribed for the given resource.
*
* @return the list of logins (maybe be empty - obviously)
*/
public List<String> selectUsersForNotification(String notificationDispatcherKey, String notificationChannelKey, @Nullable String projectUuid) {
try (DbSession session = mybatis.openSession(false)) {
return getMapper(session).findUsersForNotification(NOTIFICATION_PREFIX + notificationDispatcherKey + "." + notificationChannelKey, projectUuid);
}
}
public List<String> selectNotificationSubscribers(String notificationDispatcherKey, String notificationChannelKey, @Nullable String componentKey) {
try (DbSession session = mybatis.openSession(false)) {
return getMapper(session).findNotificationSubscribers(NOTIFICATION_PREFIX + notificationDispatcherKey + "." + notificationChannelKey, componentKey);
}
}
public boolean hasProjectNotificationSubscribersForDispatchers(String projectUuid, Collection<String> dispatcherKeys) {
try (DbSession session = mybatis.openSession(false);
Connection connection = session.getConnection();
PreparedStatement pstmt = createStatement(projectUuid, dispatcherKeys, connection);
ResultSet rs = pstmt.executeQuery()) {
return rs.next() && rs.getInt(1) > 0;
} catch (SQLException e) {
throw new IllegalStateException("Fail to execute SQL for hasProjectNotificationSubscribersForDispatchers", e);
}
}
private static PreparedStatement createStatement(String projectUuid, Collection<String> dispatcherKeys, Connection connection) throws SQLException {
String sql = "SELECT count(1) FROM properties pp " +
"left outer join projects pj on pp.resource_id = pj.id " +
"where pp.user_id is not null and (pp.resource_id is null or pj.uuid=?) " +
"and (" + DatabaseUtils.repeatCondition("pp.prop_key like ?", dispatcherKeys.size(), "or") + ")";
PreparedStatement res = connection.prepareStatement(sql);
res.setString(1, projectUuid);
int index = 2;
for (String dispatcherKey : dispatcherKeys) {
res.setString(index, "notification." + dispatcherKey + ".%");
index++;
}
return res;
}
public List<PropertyDto> selectGlobalProperties() {
try (DbSession session = mybatis.openSession(false)) {
return selectGlobalProperties(session);
}
}
public List<PropertyDto> selectGlobalProperties(DbSession session) {
return getMapper(session).selectGlobalProperties();
}
@CheckForNull
public PropertyDto selectGlobalProperty(DbSession session, String propertyKey) {
return getMapper(session).selectByKey(new PropertyDto().setKey(propertyKey));
}
@CheckForNull
public PropertyDto selectGlobalProperty(String propertyKey) {
try (DbSession session = mybatis.openSession(false)) {
return selectGlobalProperty(session, propertyKey);
}
}
public List<PropertyDto> selectProjectProperties(DbSession session, String projectKey) {
return getMapper(session).selectProjectProperties(projectKey);
}
public List<PropertyDto> selectProjectProperties(String resourceKey) {
try (DbSession session = mybatis.openSession(false)) {
return selectProjectProperties(session, resourceKey);
}
}
public List<PropertyDto> selectEnabledDescendantModuleProperties(String moduleUuid, DbSession session) {
return getMapper(session).selectDescendantModuleProperties(moduleUuid, Scopes.PROJECT, true);
}
@CheckForNull
public PropertyDto selectProjectProperty(long componentId, String propertyKey) {
try (DbSession session = mybatis.openSession(false)) {
return selectProjectProperty(session, componentId, propertyKey);
}
}
@CheckForNull
public PropertyDto selectProjectProperty(DbSession dbSession, long componentId, String propertyKey) {
return getMapper(dbSession).selectByKey(new PropertyDto().setKey(propertyKey).setResourceId(componentId));
}
public List<PropertyDto> selectByQuery(PropertyQuery query, DbSession session) {
return getMapper(session).selectByQuery(query);
}
public List<PropertyDto> selectGlobalPropertiesByKeys(DbSession session, Set<String> keys) {
return selectByKeys(session, keys, null);
}
public List<PropertyDto> selectPropertiesByKeysAndComponentId(DbSession session, Set<String> keys, long componentId) {
return selectByKeys(session, keys, componentId);
}
public List<PropertyDto> selectPropertiesByKeysAndComponentIds(DbSession session, Set<String> keys, Set<Long> componentIds) {
return executeLargeInputs(keys, partitionKeys -> executeLargeInputs(componentIds,
partitionComponentIds -> getMapper(session).selectByKeysAndComponentIds(partitionKeys, partitionComponentIds)));
}
public List<PropertyDto> selectPropertiesByComponentIds(DbSession session, Set<Long> componentIds) {
return executeLargeInputs(componentIds, getMapper(session)::selectByComponentIds);
}
private List<PropertyDto> selectByKeys(DbSession session, Set<String> keys, @Nullable Long componentId) {
return executeLargeInputs(keys, partitionKeys -> getMapper(session).selectByKeys(partitionKeys, componentId));
}
public List<PropertyDto> selectGlobalPropertiesByKeyQuery(DbSession session, String keyQuery) {
return getMapper(session).selectGlobalPropertiesByKeyQuery(buildLikeValue(keyQuery, WildcardPosition.BEFORE_AND_AFTER));
}
/**
* Saves the specified property and its value.
* <p>
* If {@link PropertyDto#getValue()} is {@code null} or empty, the properties is persisted as empty.
* </p>
*
* @throws IllegalArgumentException if {@link PropertyDto#getKey()} is {@code null} or empty
*/
public void saveProperty(DbSession session, PropertyDto property) {
save(getMapper(session), property.getKey(), property.getUserId(), property.getResourceId(), property.getValue());
}
private void save(PropertiesMapper mapper,
String key, @Nullable Integer userId, @Nullable Long componentId,
@Nullable String value) {
checkKey(key);
long now = system2.now();
mapper.delete(key, userId, componentId);
if (isEmpty(value)) {
mapper.insertAsEmpty(key, userId, componentId, now);
} else if (mustBeStoredInClob(value)) {
mapper.insertAsClob(key, userId, componentId, value, now);
} else {
mapper.insertAsText(key, userId, componentId, value, now);
}
}
private static boolean mustBeStoredInClob(String value) {
return value.length() > VARCHAR_MAXSIZE;
}
private static void checkKey(@Nullable String key) {
checkArgument(!isEmpty(key), "key can't be null nor empty");
}
private static boolean isEmpty(@Nullable String str) {
return str == null || str.isEmpty();
}
public void saveProperty(PropertyDto property) {
try (DbSession session = mybatis.openSession(false)) {
saveProperty(session, property);
session.commit();
}
}
/**
* Delete either global, user, component or component per user properties.
* <p>Behaves in exactly the same way as {@link #selectByQuery(PropertyQuery, DbSession)} but deletes rather than
* selects</p>
*
* Used by Governance.
*/
public int deleteByQuery(DbSession dbSession, PropertyQuery query) {
return getMapper(dbSession).deleteByQuery(query);
}
public int delete(DbSession dbSession, PropertyDto dto) {
return getMapper(dbSession).delete(dto.getKey(), dto.getUserId(), dto.getResourceId());
}
public void deleteProjectProperty(String key, Long projectId) {
try (DbSession session = mybatis.openSession(false)) {
deleteProjectProperty(key, projectId, session);
session.commit();
}
}
public void deleteProjectProperty(String key, Long projectId, DbSession session) {
getMapper(session).deleteProjectProperty(key, projectId);
}
public void deleteProjectProperties(String key, String value, DbSession session) {
getMapper(session).deleteProjectProperties(key, value);
}
public void deleteProjectProperties(String key, String value) {
try (DbSession session = mybatis.openSession(false)) {
deleteProjectProperties(key, value, session);
session.commit();
}
}
public void deleteGlobalProperty(String key, DbSession session) {
getMapper(session).deleteGlobalProperty(key);
}
public void deleteGlobalProperty(String key) {
try (DbSession session = mybatis.openSession(false)) {
deleteGlobalProperty(key, session);
session.commit();
}
}
public void deleteByOrganizationAndUser(DbSession dbSession, String organizationUuid, int userId) {
List<Long> ids = getMapper(dbSession).selectIdsByOrganizationAndUser(organizationUuid, userId);
executeLargeInputsWithoutOutput(ids, subList -> getMapper(dbSession).deleteByIds(subList));
}
public void deleteByOrganizationAndMatchingLogin(DbSession dbSession, String organizationUuid, String login, List<String> propertyKeys) {
List<Long> ids = getMapper(dbSession).selectIdsByOrganizationAndMatchingLogin(organizationUuid, login, propertyKeys);
executeLargeInputsWithoutOutput(ids, list -> getMapper(dbSession).deleteByIds(list));
}
public void deleteByKeyAndValue(DbSession dbSession, String key, String value){
getMapper(dbSession).deleteByKeyAndValue(key, value);
}
public void saveGlobalProperties(Map<String, String> properties) {
try (DbSession session = mybatis.openSession(false)) {
PropertiesMapper mapper = getMapper(session);
properties.entrySet().forEach(entry -> {
mapper.deleteGlobalProperty(entry.getKey());
save(mapper, entry.getKey(), null, null, entry.getValue());
});
session.commit();
}
}
public void renamePropertyKey(String oldKey, String newKey) {
checkArgument(!Strings.isNullOrEmpty(oldKey), "Old property key must not be empty");
checkArgument(!Strings.isNullOrEmpty(newKey), "New property key must not be empty");
if (!newKey.equals(oldKey)) {
try (DbSession session = mybatis.openSession(false)) {
getMapper(session).renamePropertyKey(oldKey, newKey);
session.commit();
}
}
}
private static PropertiesMapper getMapper(DbSession session) {
return session.getMapper(PropertiesMapper.class);
}
}