/*
* Copyright (C) 2008-2017, 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.Tag;
import com.juick.User;
import com.juick.server.helpers.TagStats;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
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 org.springframework.util.Assert;
import javax.inject.Inject;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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 static final Pattern TAGS_PATTERN1 = Pattern.compile("^(?:(?:\\*[^ \\r\\n\\t]+)|\\s)+$");
private static final Pattern TAGS_PATTERN2 = Pattern.compile("^\\*([^ \\r\\n\\t]+)\\s+([\\s\\S]+)");
private static final Pattern TAG_PATTERN = Pattern.compile("\\*([^ \\r\\n\\t]+)");
private final MessagesService messagesService;
@Inject
public TagServiceImpl(MessagesService messagesService) {
Assert.notNull(messagesService, "MessagesService must be initialized");
this.messagesService = messagesService;
}
@Transactional(readOnly = true)
@Override
public com.juick.Tag getTag(final int tid) {
List list = getJdbcTemplate().query(
"SELECT synonym_id,name FROM tags WHERE tag_id=?",
(rs, num) -> {
Tag ret = new Tag(rs.getString(2));
ret.TID = tid;
ret.SynonymID = rs.getInt(1);
return ret;
},
tid);
return list.isEmpty() ?
null : list.get(0);
}
@Transactional
@Override
public com.juick.Tag getTag(final String tag, final boolean autoCreate) {
if (StringUtils.isBlank(tag))
return null;
List list = getJdbcTemplate().query(
"SELECT tag_id, synonym_id, name FROM tags WHERE name = ?",
(rs, rowNum) -> {
Tag ret1 = new Tag(rs.getString(3));
ret1.TID = rs.getInt(1);
ret1.SynonymID = rs.getInt(2);
return ret1;
},
tag);
Tag ret = list.isEmpty() ?
null : list.get(0);
if (ret == null && autoCreate) {
ret = new com.juick.Tag(tag);
ret.TID = createTag(tag);
}
return ret;
}
@Override
public List getTags(Stream tags, final boolean autoCreate) {
return tags.filter(StringUtils::isNotBlank).map(tag -> getTag(tag, autoCreate)).filter(Objects::nonNull).distinct()
.collect(Collectors.toList());
}
@Transactional(readOnly = true)
@Override
public boolean getTagNoIndex(final int tagId) {
List list = getJdbcTemplate().queryForList(
"SELECT noindex FROM tags WHERE tag_id=?", Integer.class, tagId);
return !list.isEmpty() && list.get(0) == 1;
}
@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.getKey().intValue();
}
private 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 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 top=1 ORDER BY name ASC", String.class)
.stream()
.collect(Collectors.toList());
}
@Transactional(readOnly = true)
@Override
public List getTagStats() {
return getJdbcTemplate().query(
"SELECT tags.name,COUNT(DISTINCT messages.user_id) AS cnt " +
"FROM (messages INNER JOIN messages_tags ON (messages.ts>TIMESTAMPADD(DAY,-3,NOW()) " +
"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 ORDER BY cnt DESC LIMIT 20", new TagStatsMapper());
}
@Transactional
@Override
public List updateTags(final int mid, final Collection newTags) {
List currentTags = getMessageTags(mid).stream()
.map(TagStats::getTag).collect(Collectors.toList());
if (CollectionUtils.isEmpty(newTags))
return currentTags;
List idsForDelete = newTags.stream()
.filter(currentTags::contains)
.map(tag -> tag.TID)
.collect(Collectors.toList());
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));
newTags.stream().filter(t -> !currentTags.contains(t))
.forEach(t -> getJdbcTemplate().update("INSERT INTO messages_tags(message_id,tag_id) VALUES (?,?)", mid, t.TID));
return getMessageTags(mid).stream()
.map(TagStats::getTag).collect(Collectors.toList());
}
@Override
public List fromString(final String txt, final boolean tagsOnly) {
Pattern tagsPattern = tagsOnly ? TAGS_PATTERN1 : TAGS_PATTERN2;
if (tagsPattern.matcher(txt).matches()) {
Matcher tagMatcher = TAG_PATTERN.matcher(txt);
List tags = new ArrayList<>();
// TODO: process readonly, private, friends, public
while (tagMatcher.find()) {
for (int i = 1; i <= tagMatcher.groupCount(); i++)
tags.add(getTag(tagMatcher.group(i), true));
}
return tags;
}
return Collections.emptyList();
}
@Transactional(readOnly = true)
@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) -> {
com.juick.Tag t = new com.juick.Tag(rs.getString(3));
t.TID = rs.getInt(1);
t.SynonymID = rs.getInt(2);
TagStats s = new TagStats();
s.setTag(t);
s.setUsageCount(rs.getInt(4));
return s;
}, mid);
}
@Transactional(readOnly = true)
@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.TID).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.TID)
.addValue("uid", user.getUid())) > 0;
}
@Transactional(readOnly = true)
@Override
public boolean isInBL(User user, Tag tag) {
List list = getJdbcTemplate().queryForList(
"SELECT 1 FROM bl_tags WHERE user_id = ? AND tag_id = ?",
Integer.class, user.getUid(), tag.TID);
return !list.isEmpty() && list.get(0) == 1;
}
@Transactional(readOnly = true)
@Override
public boolean isSubscribed(User user, Tag tag) {
List list = getJdbcTemplate().queryForList(
"SELECT 1 FROM subscr_tags WHERE suser_id = ? AND tag_id = ?",
Integer.class, user.getUid(), tag.TID);
return !list.isEmpty() && list.get(0) == 1;
}
}