/*
* Copyright (C) 2008-2023, Juick
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package com.juick.service;
import com.juick.model.Tag;
import com.juick.model.TagStats;
import com.juick.model.User;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Created by aalexeev on 11/13/16.
*/
@Repository
public class TagServiceImpl extends BaseJdbcService implements TagService {
private class TagMapper implements RowMapper {
@Override
public Tag mapRow(ResultSet rs, int rowNum) throws SQLException {
Tag tag = new Tag(rs.getString(3));
tag.setId(rs.getInt(1));
tag.setSynonymId(rs.getInt(2));
tag.setNotIndexed(rs.getInt("noindex") > 0);
tag.setHiddenFromTop(rs.getInt("notop") > 0);
tag.setSubscribed(rs.getInt("subscribed") > 0);
tag.setIgnored(rs.getInt("ignored") > 0);
return tag;
}
}
@Override
public Tag getTag(int tid) {
return getTag(0, tid);
}
@Transactional(readOnly = true)
@Override
public Tag getTag(int userId, int tid) {
List list = namedParameterJdbcTemplate.query("""
SELECT tag_id, synonym_id, name, noindex, notop,
COALESCE((SELECT 1 FROM bl_tags WHERE tag_id=:tagId AND user_id=:userId), 0) ignored,
COALESCE((SELECT 1 FROM subscr_tags WHERE tag_id=:tagId AND suser_id=:userId), 0) subscribed
FROM tags WHERE tag_id = :tagId""",
new MapSqlParameterSource()
.addValue("userId", userId)
.addValue("tagId", tid),
new TagMapper());
return list.isEmpty() ? null : list.get(0);
}
@Override
public Tag getTag(final String tagName, boolean autoCreate) {
return getTag(0, tagName, autoCreate);
}
@Transactional
@Override
public Tag getTag(int userId, final String tagName, boolean autoCreate) {
if (StringUtils.isBlank(tagName))
return null;
List list = namedParameterJdbcTemplate.query("""
SELECT t.tag_id, t.synonym_id, t.name, t.noindex, t.notop,
COALESCE((SELECT 1 FROM bl_tags WHERE tag_id=t.tag_id AND user_id=:userId), 0) ignored,
COALESCE((SELECT 1 FROM subscr_tags WHERE tag_id=t.tag_id AND suser_id=:userId), 0) subscribed
FROM (SELECT tag_id, synonym_id, name, noindex, notop FROM tags) t
WHERE name = :tagName""",
new MapSqlParameterSource()
.addValue("userId", userId)
.addValue("tagName", tagName),
new TagMapper());
Tag ret = list.isEmpty() ? null : list.get(0);
if (ret == null && autoCreate) {
ret = new Tag(tagName);
ret.setId(createTag(tagName));
}
return ret;
}
@Override
public List getTags(Stream tags, final boolean autoCreate) {
return getTags(0, tags, autoCreate);
}
@Override
public List getTags(int userId, Stream tags, final boolean autoCreate) {
return tags.filter(StringUtils::isNotBlank).map(tag -> getTag(tag, autoCreate)).filter(Objects::nonNull)
.distinct()
.toList();
}
@Transactional
@Override
public int createTag(final String name) {
KeyHolder holder = new GeneratedKeyHolder();
getJdbcTemplate().update(
con -> {
PreparedStatement stmt = con.prepareStatement(
"INSERT INTO tags(name) VALUES (?)",
Statement.RETURN_GENERATED_KEYS);
stmt.setString(1, name);
return stmt;
},
holder);
return holder.getKeys().size() > 1 ? ((Number) holder.getKeys().get("tag_id")).intValue()
: holder.getKey().intValue();
}
private static class TagStatsMapper implements RowMapper {
@Override
public TagStats mapRow(ResultSet rs, int rowNum) throws SQLException {
Tag t = new Tag(rs.getString(1));
TagStats s = new TagStats();
s.setTag(t);
s.setUsageCount(rs.getInt(2));
return s;
}
}
@Transactional(readOnly = true)
@Override
public List getUserTagStats(final int uid) {
return getJdbcTemplate().query(
"SELECT tags.name,COUNT(messages.message_id) " +
"FROM (messages INNER JOIN messages_tags ON (messages.user_id=? " +
"AND messages.message_id=messages_tags.message_id)) " +
"INNER JOIN tags ON messages_tags.tag_id=tags.tag_id GROUP BY tags.tag_id, tags.name ORDER BY tags.name ASC",
new TagStatsMapper(),
uid);
}
@Transactional(readOnly = true)
@Override
public List getUserBLTags(final int uid) {
return getJdbcTemplate().queryForList(
"SELECT tags.name FROM tags INNER JOIN bl_tags " +
"ON (bl_tags.user_id = ? AND bl_tags.tag_id = tags.tag_id) ORDER BY tags.name",
String.class, uid);
}
@Transactional(readOnly = true)
@Override
public List getPopularTags() {
return getJdbcTemplate().queryForList("""
select name from tags where noindex=0
order by stat_messages desc
""" + limit(20), String.class);
}
@Transactional(readOnly = true)
@Override
public List getTagStats() {
var ts = Instant.now().minus(14, ChronoUnit.DAYS);
var sql = """
SELECT tags.name,COUNT(DISTINCT messages.user_id) AS cnt
FROM (messages INNER JOIN messages_tags ON (messages.ts > :ts
AND messages.message_id=messages_tags.message_id))
INNER JOIN tags ON messages_tags.tag_id=tags.tag_id
WHERE tags.tag_id NOT IN (SELECT tag_id FROM tags_ignore)
GROUP BY tags.tag_id, tags.name HAVING COUNT(DISTINCT messages.user_id) > 1
ORDER BY cnt DESC
""" + limit(20);
return getNamedParameterJdbcTemplate()
.query(sql, new MapSqlParameterSource()
.addValue("ts", toDateTime(ts.atOffset(ZoneOffset.UTC)), dateTimeType()),
new TagStatsMapper());
}
@Transactional
@Override
public Set updateTags(final int mid, final Collection newTags) {
Set currentTags = getMessageTags(mid).stream()
.map(TagStats::getTag).collect(Collectors.toSet());
if (CollectionUtils.isEmpty(newTags))
return currentTags;
List idsForDelete = newTags.stream()
.filter(currentTags::contains)
.map(Tag::getId)
.toList();
if (newTags.size() - idsForDelete.size() >= 5) {
return currentTags;
}
if (!idsForDelete.isEmpty())
getNamedParameterJdbcTemplate().update(
"DELETE FROM messages_tags WHERE message_id = :mid AND tag_id in (:ids)",
new MapSqlParameterSource().addValue("ids", idsForDelete).addValue("mid", mid));
List addedTags = newTags.stream().filter(t -> !currentTags.contains(t)).toList();
getJdbcTemplate().batchUpdate("INSERT INTO messages_tags(message_id,tag_id) VALUES (?,?)",
new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
ps.setInt(1, mid);
ps.setInt(2, addedTags.get(i).getId());
}
@Override
public int getBatchSize() {
return addedTags.size();
}
});
return getMessageTags(mid).stream()
.map(TagStats::getTag).collect(Collectors.toSet());
}
@Override
public Pair> fromString(final String txt) {
String firstLine = txt.split("\\n", 2)[0];
Supplier> tagsStream = () -> Arrays.stream(firstLine.split(" "))
.takeWhile(t -> t.matches("\\*\\S+|#\\S*\\D\\S*"));
int tagsLength = tagsStream.get().collect(Collectors.joining(" ")).length();
String body = txt.substring(tagsLength);
Set tags = tagsStream.get().map(t -> getTag(t.substring(1), true)).collect(Collectors.toSet());
return Pair.of(body, tags);
}
@Override
public List getMessageTags(final int mid) {
return getJdbcTemplate().query(
"SELECT tags.tag_id,synonym_id,name,stat_messages FROM tags " +
"INNER JOIN messages_tags ON (messages_tags.message_id = ? AND messages_tags.tag_id = tags.tag_id)",
(rs, num) -> {
Tag t = new Tag(rs.getString(3));
t.setId(rs.getInt(1));
t.setSynonymId(rs.getInt(2));
TagStats s = new TagStats();
s.setTag(t);
s.setUsageCount(rs.getInt(4));
return s;
}, mid);
}
@Override
public List getMessageTagsIDs(final int mid) {
return getJdbcTemplate().queryForList(
"SELECT tag_id FROM messages_tags WHERE message_id = ?",
Integer.class, mid);
}
@Override
public boolean blacklistTag(User user, Tag tag) {
int rowcount = getNamedParameterJdbcTemplate().update(
"DELETE FROM bl_tags WHERE tag_id = :tid AND user_id = :uid",
new MapSqlParameterSource().addValue("tid", tag.getId()).addValue("uid", user.getUid()));
return rowcount <= 0 && getNamedParameterJdbcTemplate()
.update("INSERT INTO bl_tags(user_id, tag_id) VALUES(:uid,:tid)",
new MapSqlParameterSource().addValue("tid", tag.getId())
.addValue("uid", user.getUid())) > 0;
}
}