/*
* Symphony - A modern community (forum/SNS/blog) platform written in Java.
* Copyright (C) 2012-2017, b3log.org & hacpai.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.b3log.symphony.service;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.b3log.latke.Keys;
import org.b3log.latke.event.Event;
import org.b3log.latke.event.EventException;
import org.b3log.latke.event.EventManager;
import org.b3log.latke.ioc.inject.Inject;
import org.b3log.latke.logging.Level;
import org.b3log.latke.logging.Logger;
import org.b3log.latke.model.User;
import org.b3log.latke.repository.*;
import org.b3log.latke.repository.annotation.Transactional;
import org.b3log.latke.repository.jdbc.JdbcRepository;
import org.b3log.latke.service.LangPropsService;
import org.b3log.latke.service.ServiceException;
import org.b3log.latke.service.annotation.Service;
import org.b3log.latke.thread.ThreadService;
import org.b3log.latke.thread.ThreadServiceFactory;
import org.b3log.latke.util.Ids;
import org.b3log.symphony.event.EventTypes;
import org.b3log.symphony.model.*;
import org.b3log.symphony.repository.*;
import org.b3log.symphony.util.Emotions;
import org.b3log.symphony.util.Pangu;
import org.b3log.symphony.util.Runes;
import org.b3log.symphony.util.Symphonys;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
/**
* Article management service.
*
* @author <a href="http://88250.b3log.org">Liang Ding</a>
* @author <a href="http://zephyr.b3log.org">Zephyr</a>
* @version 2.17.34.43, May 8, 2017
* @since 0.2.0
*/
@Service
public class ArticleMgmtService {
/**
* Logger.
*/
private static final Logger LOGGER = Logger.getLogger(ArticleMgmtService.class);
/**
* Tag max count.
*/
private static final int TAG_MAX_CNT = 4;
/**
* Comment repository.
*/
@Inject
private CommentRepository commentRepository;
/**
* Article repository.
*/
@Inject
private ArticleRepository articleRepository;
/**
* Tag repository.
*/
@Inject
private TagRepository tagRepository;
/**
* Tag-Article repository.
*/
@Inject
private TagArticleRepository tagArticleRepository;
/**
* User repository.
*/
@Inject
private UserRepository userRepository;
/**
* User-Tag repository.
*/
@Inject
private UserTagRepository userTagRepository;
/**
* Option repository.
*/
@Inject
private OptionRepository optionRepository;
/**
* Notification repository.
*/
@Inject
private NotificationRepository notificationRepository;
/**
* Revision repository.
*/
@Inject
private RevisionRepository revisionRepository;
/**
* Tag management service.
*/
@Inject
private TagMgmtService tagMgmtService;
/**
* Event manager.
*/
@Inject
private EventManager eventManager;
/**
* Language service.
*/
@Inject
private LangPropsService langPropsService;
/**
* Pointtransfer management service.
*/
@Inject
private PointtransferMgmtService pointtransferMgmtService;
/**
* Reward management service.
*/
@Inject
private RewardMgmtService rewardMgmtService;
/**
* Reward query service.
*/
@Inject
private RewardQueryService rewardQueryService;
/**
* Follow query service.
*/
@Inject
private FollowQueryService followQueryService;
/**
* Tag query service.
*/
@Inject
private TagQueryService tagQueryService;
/**
* Notification management service.
*/
@Inject
private NotificationMgmtService notificationMgmtService;
/**
* Liveness management service.
*/
@Inject
private LivenessMgmtService livenessMgmtService;
/**
* Search management service.
*/
@Inject
private SearchMgmtService searchMgmtService;
/**
* Audio management service.
*/
@Inject
private AudioMgmtService audioMgmtService;
/**
* Determines whether the specified tag title exists in the specified tags.
*
* @param tagTitle the specified tag title
* @param tags the specified tags
* @return {@code true} if it exists, {@code false} otherwise
* @throws JSONException json exception
*/
private static boolean tagExists(final String tagTitle, final List<JSONObject> tags) throws JSONException {
for (final JSONObject tag : tags) {
if (tag.getString(Tag.TAG_TITLE).equals(tagTitle)) {
return true;
}
}
return false;
}
/**
* Removes an article specified with the given article id. An article is removable if:
* <ul>
* <li>No comments</li>
* <li>No watches, collects, ups, downs</li>
* <li>No rewards</li>
* <li>No thanks</li>
* </ul>
* Sees https://github.com/b3log/symphony/issues/450 for more details.
*
* @param articleId the given article id
* @throws ServiceException service exception
*/
public void removeArticle(final String articleId) throws ServiceException {
JSONObject article = null;
try {
article = articleRepository.get(articleId);
} catch (final Exception e) {
LOGGER.log(Level.ERROR, "Gets article [id=" + articleId + "] failed", e);
}
if (null == article) {
return;
}
final int commentCnt = article.optInt(Article.ARTICLE_COMMENT_CNT);
if (commentCnt > 0) {
throw new ServiceException(langPropsService.get("removeArticleFoundCmtLabel"));
}
final int watchCnt = article.optInt(Article.ARTICLE_WATCH_CNT);
final int collectCnt = article.optInt(Article.ARTICLE_COLLECT_CNT);
final int ups = article.optInt(Article.ARTICLE_GOOD_CNT);
final int downs = article.optInt(Article.ARTICLE_BAD_CNT);
if (watchCnt > 0 || collectCnt > 0 || ups > 0 || downs > 0) {
throw new ServiceException("removeArticleFoundWatchEtcLabel");
}
final int rewardCnt = (int) rewardQueryService.rewardedCount(articleId, Reward.TYPE_C_ARTICLE);
if (rewardCnt > 0) {
throw new ServiceException("removeArticleFoundRewardLabel");
}
final int thankCnt = (int) rewardQueryService.rewardedCount(articleId, Reward.TYPE_C_THANK_ARTICLE);
if (thankCnt > 0) {
throw new ServiceException("removeArticleFoundThankLabel");
}
// Perform removal
removeArticleByAdmin(articleId);
}
/**
* Generates article's audio.
*
* @param article the specified article
* @param userId the specified user id
*/
public void genArticleAudio(final JSONObject article, final String userId) {
if (Article.ARTICLE_TYPE_C_THOUGHT == article.optInt(Article.ARTICLE_TYPE)) {
return;
}
final String articleId = article.optString(Keys.OBJECT_ID);
String previewContent = article.optString(Article.ARTICLE_CONTENT);
previewContent = Emotions.clear(Jsoup.parse(previewContent).text());
previewContent = StringUtils.substring(previewContent, 0, 512);
final String contentToTTS = previewContent;
final ThreadService threadService = ThreadServiceFactory.getThreadService();
threadService.submit(() -> {
final Transaction transaction = articleRepository.beginTransaction();
String audioURL = "";
if (StringUtils.length(contentToTTS) < 96 || Runes.getChinesePercent(contentToTTS) < 40) {
LOGGER.trace("Content is too short to TTS [contentToTTS=" + contentToTTS + "]");
} else {
audioURL = audioMgmtService.tts(contentToTTS, Article.ARTICLE, articleId, userId);
}
article.put(Article.ARTICLE_AUDIO_URL, audioURL);
try {
final JSONObject toUpdate = articleRepository.get(articleId);
toUpdate.put(Article.ARTICLE_AUDIO_URL, audioURL);
articleRepository.update(articleId, toUpdate);
transaction.commit();
if (StringUtils.isNotBlank(audioURL)) {
LOGGER.debug("Generated article [id=" + articleId + "] audio");
}
} catch (final Exception e) {
if (transaction.isActive()) {
transaction.rollback();
}
LOGGER.log(Level.ERROR, "Updates article's audio URL failed", e);
}
JdbcRepository.dispose();
}, 1000 * 30);
}
/**
* Removes an article specified with the given article id. Calls this method will remove all existed data related
* with the specified article forcibly.
*
* @param articleId the given article id
*/
@Transactional
public void removeArticleByAdmin(final String articleId) {
try {
final JSONObject article = articleRepository.get(articleId);
if (null == article) {
return;
}
Query query = new Query().setFilter(new PropertyFilter(
Comment.COMMENT_ON_ARTICLE_ID, FilterOperator.EQUAL, articleId)).setPageCount(1);
final JSONArray comments = commentRepository.get(query).optJSONArray(Keys.RESULTS);
final int commentCnt = comments.length();
for (int i = 0; i < commentCnt; i++) {
final JSONObject comment = comments.optJSONObject(i);
final String commentId = comment.optString(Keys.OBJECT_ID);
commentRepository.removeComment(commentId);
}
final String authorId = article.optString(Article.ARTICLE_AUTHOR_ID);
final JSONObject author = userRepository.get(authorId);
author.put(UserExt.USER_ARTICLE_COUNT, author.optInt(UserExt.USER_ARTICLE_COUNT) - 1);
userRepository.update(author.optString(Keys.OBJECT_ID), author);
final String city = article.optString(Article.ARTICLE_CITY);
final String cityStatId = city + "-ArticleCount";
final JSONObject cityArticleCntOption = optionRepository.get(cityStatId);
if (null != cityArticleCntOption) {
cityArticleCntOption.put(Option.OPTION_VALUE, cityArticleCntOption.optInt(Option.OPTION_VALUE) - 1);
optionRepository.update(cityStatId, cityArticleCntOption);
}
final JSONObject articleCntOption = optionRepository.get(Option.ID_C_STATISTIC_ARTICLE_COUNT);
articleCntOption.put(Option.OPTION_VALUE, articleCntOption.optInt(Option.OPTION_VALUE) - 1);
optionRepository.update(Option.ID_C_STATISTIC_ARTICLE_COUNT, articleCntOption);
articleRepository.remove(articleId);
// Remove article revisions
query = new Query().setFilter(CompositeFilterOperator.and(
new PropertyFilter(Revision.REVISION_DATA_ID, FilterOperator.EQUAL, articleId),
new PropertyFilter(Revision.REVISION_DATA_TYPE, FilterOperator.EQUAL, Revision.DATA_TYPE_C_ARTICLE)
));
final JSONArray articleRevisions = revisionRepository.get(query).optJSONArray(Keys.RESULTS);
for (int i = 0; i < articleRevisions.length(); i++) {
final JSONObject articleRevision = articleRevisions.optJSONObject(i);
revisionRepository.remove(articleRevision.optString(Keys.OBJECT_ID));
}
final List<JSONObject> tagArticleRels = tagArticleRepository.getByArticleId(articleId);
for (final JSONObject tagArticleRel : tagArticleRels) {
final String tagId = tagArticleRel.optString(Tag.TAG + "_" + Keys.OBJECT_ID);
final JSONObject tag = tagRepository.get(tagId);
int cnt = tag.optInt(Tag.TAG_REFERENCE_CNT) - 1;
cnt = cnt < 0 ? 0 : cnt;
tag.put(Tag.TAG_REFERENCE_CNT, cnt);
tag.put(Tag.TAG_RANDOM_DOUBLE, Math.random());
tagRepository.update(tagId, tag);
}
tagArticleRepository.removeByArticleId(articleId);
notificationRepository.removeByDataId(articleId);
if (Symphonys.getBoolean("algolia.enabled")) {
searchMgmtService.removeAlgoliaDocument(article);
}
if (Symphonys.getBoolean("es.enabled")) {
searchMgmtService.removeESDocument(article, Article.ARTICLE);
}
} catch (final RepositoryException e) {
LOGGER.log(Level.ERROR, "Removes an article error [id=" + articleId + "]", e);
}
}
/**
* Increments the view count of the specified article by the given article id.
*
* @param articleId the given article id
* @throws ServiceException service exception
*/
public void incArticleViewCount(final String articleId) throws ServiceException {
Symphonys.EXECUTOR_SERVICE.submit(new Runnable() {
@Override
public void run() {
final Transaction transaction = articleRepository.beginTransaction();
try {
final JSONObject article = articleRepository.get(articleId);
if (null == article) {
return;
}
final int viewCnt = article.optInt(Article.ARTICLE_VIEW_CNT);
article.put(Article.ARTICLE_VIEW_CNT, viewCnt + 1);
article.put(Article.ARTICLE_RANDOM_DOUBLE, Math.random());
articleRepository.update(articleId, article);
transaction.commit();
} catch (final RepositoryException e) {
if (transaction.isActive()) {
transaction.rollback();
}
LOGGER.log(Level.ERROR, "Incs an article view count failed", e);
}
}
});
}
/**
* Adds an article with the specified request json object.
*
* @param requestJSONObject the specified request json object, for example,
* "articleTitle": "",
* "articleTags": "",
* "articleContent": "",
* "articleEditorType": "",
* "articleAuthorId": "",
* "articleCommentable": boolean, // optional, default to true
* "syncWithSymphonyClient": boolean, // optional
* "clientArticleId": "", // optional
* "clientArticlePermalink": "", // optional
* "isBroadcast": boolean, // Client broadcast, optional
* "articleType": int, // optional, default to 0
* "articleRewardContent": "", // optional, default to ""
* "articleRewardPoint": int, // optional, default to 0
* "articleIP": "", // optional, default to ""
* "articleUA": "", // optional, default to ""
* "articleAnonymous": int, // optional, default to 0 (public)
* "articleAnonymousView": int // optional, default to 0 (use global)
* , see {@link Article} for more details
* @return generated article id
* @throws ServiceException service exception
*/
public synchronized String addArticle(final JSONObject requestJSONObject) throws ServiceException {
final long currentTimeMillis = System.currentTimeMillis();
final boolean fromClient = requestJSONObject.has(Article.ARTICLE_CLIENT_ARTICLE_ID);
final String authorId = requestJSONObject.optString(Article.ARTICLE_AUTHOR_ID);
JSONObject author = null;
final int rewardPoint = requestJSONObject.optInt(Article.ARTICLE_REWARD_POINT, 0);
if (rewardPoint < 0) {
throw new ServiceException(langPropsService.get("invalidRewardPointLabel"));
}
final int articleAnonymous = requestJSONObject.optInt(Article.ARTICLE_ANONYMOUS);
final boolean syncWithSymphonyClient = requestJSONObject.optBoolean(Article.ARTICLE_SYNC_TO_CLIENT);
String articleTitle = requestJSONObject.optString(Article.ARTICLE_TITLE);
articleTitle = Emotions.toAliases(articleTitle);
articleTitle = Pangu.spacingText(articleTitle);
articleTitle = StringUtils.trim(articleTitle);
final int articleType = requestJSONObject.optInt(Article.ARTICLE_TYPE, Article.ARTICLE_TYPE_C_NORMAL);
try {
// check if admin allow to add article
final JSONObject option = optionRepository.get(Option.ID_C_MISC_ALLOW_ADD_ARTICLE);
if (!"0".equals(option.optString(Option.OPTION_VALUE))) {
throw new ServiceException(langPropsService.get("notAllowAddArticleLabel"));
}
author = userRepository.get(authorId);
if (currentTimeMillis - author.optLong(UserExt.USER_LATEST_ARTICLE_TIME) < Symphonys.getLong("minStepArticleTime")
&& !Role.ROLE_ID_C_ADMIN.equals(author.optString(User.USER_ROLE))) {
LOGGER.log(Level.WARN, "Adds article too frequent [userName={0}]", author.optString(User.USER_NAME));
throw new ServiceException(langPropsService.get("tooFrequentArticleLabel"));
}
final int balance = author.optInt(UserExt.USER_POINT);
if (Article.ARTICLE_ANONYMOUS_C_ANONYMOUS == articleAnonymous) {
final int anonymousPoint = Symphonys.getInt("anonymous.point");
if (balance < anonymousPoint) {
String anonymousEnabelPointLabel = langPropsService.get("anonymousEnabelPointLabel");
anonymousEnabelPointLabel
= anonymousEnabelPointLabel.replace("${point}", String.valueOf(anonymousPoint));
throw new ServiceException(anonymousEnabelPointLabel);
}
}
if (!fromClient && Article.ARTICLE_ANONYMOUS_C_PUBLIC == articleAnonymous) {
// Point
final long followerCnt = followQueryService.getFollowerCount(authorId, Follow.FOLLOWING_TYPE_C_USER);
final int addition = (int) Math.round(Math.sqrt(followerCnt));
final int broadcast = Article.ARTICLE_TYPE_C_CITY_BROADCAST == articleType ?
Pointtransfer.TRANSFER_SUM_C_ADD_ARTICLE_BROADCAST : 0;
final int sum = Pointtransfer.TRANSFER_SUM_C_ADD_ARTICLE + addition + rewardPoint + broadcast;
if (balance - sum < 0) {
throw new ServiceException(langPropsService.get("insufficientBalanceLabel"));
}
}
if (Article.ARTICLE_TYPE_C_DISCUSSION != articleType && Article.ARTICLE_TYPE_C_BOOK != articleType) {
final JSONObject maybeExist = articleRepository.getByTitle(articleTitle);
if (null != maybeExist) {
final String existArticleAuthorId = maybeExist.optString(Article.ARTICLE_AUTHOR_ID);
String msg;
if (existArticleAuthorId.equals(authorId)) {
msg = langPropsService.get("duplicatedArticleTitleSelfLabel");
msg = msg.replace("{article}", "<a target='_blank' href='/article/" + maybeExist.optString(Keys.OBJECT_ID)
+ "'>" + articleTitle + "</a>");
} else {
final JSONObject existArticleAuthor = userRepository.get(existArticleAuthorId);
final String userName = existArticleAuthor.optString(User.USER_NAME);
msg = langPropsService.get("duplicatedArticleTitleLabel");
msg = msg.replace("{user}", "<a target='_blank' href='/member/" + userName + "'>" + userName + "</a>");
msg = msg.replace("{article}", "<a target='_blank' href='/article/" + maybeExist.optString(Keys.OBJECT_ID)
+ "'>" + articleTitle + "</a>");
}
throw new ServiceException(msg);
}
}
} catch (final RepositoryException e) {
throw new ServiceException(e);
}
final Transaction transaction = articleRepository.beginTransaction();
try {
final String ret = Ids.genTimeMillisId();
final JSONObject article = new JSONObject();
article.put(Keys.OBJECT_ID, ret);
final String clientArticleId = requestJSONObject.optString(Article.ARTICLE_CLIENT_ARTICLE_ID, ret);
final String clientArticlePermalink = requestJSONObject.optString(Article.ARTICLE_CLIENT_ARTICLE_PERMALINK);
final boolean isBroadcast = requestJSONObject.optBoolean(Article.ARTICLE_T_IS_BROADCAST);
article.put(Article.ARTICLE_TITLE, articleTitle);
article.put(Article.ARTICLE_TAGS, requestJSONObject.optString(Article.ARTICLE_TAGS));
String articleContent = requestJSONObject.optString(Article.ARTICLE_CONTENT);
articleContent = Emotions.toAliases(articleContent);
//articleContent = StringUtils.trim(articleContent) + " "; https://github.com/b3log/symphony/issues/389
articleContent = StringUtils.replace(articleContent, langPropsService.get("uploadingLabel", Locale.SIMPLIFIED_CHINESE), "");
articleContent = StringUtils.replace(articleContent, langPropsService.get("uploadingLabel", Locale.US), "");
article.put(Article.ARTICLE_CONTENT, articleContent);
article.put(Article.ARTICLE_REWARD_CONTENT, requestJSONObject.optString(Article.ARTICLE_REWARD_CONTENT));
article.put(Article.ARTICLE_EDITOR_TYPE, requestJSONObject.optString(Article.ARTICLE_EDITOR_TYPE));
article.put(Article.ARTICLE_SYNC_TO_CLIENT, fromClient ? true : author.optBoolean(UserExt.SYNC_TO_CLIENT));
article.put(Article.ARTICLE_AUTHOR_ID, authorId);
article.put(Article.ARTICLE_COMMENT_CNT, 0);
article.put(Article.ARTICLE_VIEW_CNT, 0);
article.put(Article.ARTICLE_GOOD_CNT, 0);
article.put(Article.ARTICLE_BAD_CNT, 0);
article.put(Article.ARTICLE_COLLECT_CNT, 0);
article.put(Article.ARTICLE_WATCH_CNT, 0);
article.put(Article.ARTICLE_COMMENTABLE, requestJSONObject.optBoolean(Article.ARTICLE_COMMENTABLE, true));
article.put(Article.ARTICLE_CREATE_TIME, currentTimeMillis);
article.put(Article.ARTICLE_UPDATE_TIME, currentTimeMillis);
article.put(Article.ARTICLE_LATEST_CMT_TIME, 0);
article.put(Article.ARTICLE_LATEST_CMTER_NAME, "");
article.put(Article.ARTICLE_PERMALINK, "/article/" + ret);
if (isBroadcast) {
article.put(Article.ARTICLE_CLIENT_ARTICLE_ID, "aBroadcast");
} else {
article.put(Article.ARTICLE_CLIENT_ARTICLE_ID, clientArticleId);
}
article.put(Article.ARTICLE_CLIENT_ARTICLE_PERMALINK, clientArticlePermalink);
article.put(Article.ARTICLE_RANDOM_DOUBLE, Math.random());
article.put(Article.REDDIT_SCORE, 0);
article.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_VALID);
article.put(Article.ARTICLE_TYPE, articleType);
article.put(Article.ARTICLE_REWARD_POINT, rewardPoint);
String city = "";
if (UserExt.USER_GEO_STATUS_C_PUBLIC == author.optInt(UserExt.USER_GEO_STATUS)) {
city = author.optString(UserExt.USER_CITY);
}
article.put(Article.ARTICLE_CITY, city);
article.put(Article.ARTICLE_ANONYMOUS, articleAnonymous);
article.put(Article.ARTICLE_SYNC_TO_CLIENT, syncWithSymphonyClient);
article.put(Article.ARTICLE_PERFECT, Article.ARTICLE_PERFECT_C_NOT_PERFECT);
article.put(Article.ARTICLE_ANONYMOUS_VIEW,
requestJSONObject.optInt(Article.ARTICLE_ANONYMOUS_VIEW, Article.ARTICLE_ANONYMOUS_VIEW_C_USE_GLOBAL));
article.put(Article.ARTICLE_AUDIO_URL, "");
String articleTags = article.optString(Article.ARTICLE_TAGS);
articleTags = Tag.formatTags(articleTags);
boolean sandboxEnv = false;
if (StringUtils.containsIgnoreCase(articleTags, Tag.TAG_TITLE_C_SANDBOX)) {
articleTags = Tag.TAG_TITLE_C_SANDBOX;
sandboxEnv = true;
}
String[] tagTitles = articleTags.split(",");
if (!sandboxEnv && tagTitles.length < TAG_MAX_CNT && tagTitles.length < 3
&& Article.ARTICLE_TYPE_C_DISCUSSION != articleType
&& Article.ARTICLE_TYPE_C_THOUGHT != articleType && !Tag.containsReservedTags(articleTags)) {
final String content = article.optString(Article.ARTICLE_TITLE)
+ " " + Jsoup.parse("<p>" + article.optString(Article.ARTICLE_CONTENT) + "</p>").text();
final List<String> genTags = tagQueryService.generateTags(content, 1);
if (!genTags.isEmpty()) {
articleTags = articleTags + "," + StringUtils.join(genTags, ",");
articleTags = Tag.formatTags(articleTags);
articleTags = Tag.useHead(articleTags, TAG_MAX_CNT);
}
}
if (StringUtils.isBlank(articleTags)) {
articleTags = "B3log";
}
articleTags = Tag.formatTags(articleTags);
article.put(Article.ARTICLE_TAGS, articleTags);
tagTitles = articleTags.split(",");
tag(tagTitles, article, author);
final String ip = requestJSONObject.optString(Article.ARTICLE_IP);
article.put(Article.ARTICLE_IP, ip);
String ua = requestJSONObject.optString(Article.ARTICLE_UA);
if (StringUtils.length(ua) > Common.MAX_LENGTH_UA) {
ua = StringUtils.substring(ua, 0, Common.MAX_LENGTH_UA);
}
article.put(Article.ARTICLE_UA, ua);
article.put(Article.ARTICLE_STICK, 0L);
final JSONObject articleCntOption = optionRepository.get(Option.ID_C_STATISTIC_ARTICLE_COUNT);
final int articleCnt = articleCntOption.optInt(Option.OPTION_VALUE);
articleCntOption.put(Option.OPTION_VALUE, articleCnt + 1);
optionRepository.update(Option.ID_C_STATISTIC_ARTICLE_COUNT, articleCntOption);
if (!StringUtils.isBlank(city)) {
final String cityStatId = city + "-ArticleCount";
JSONObject cityArticleCntOption = optionRepository.get(cityStatId);
if (null == cityArticleCntOption) {
cityArticleCntOption = new JSONObject();
cityArticleCntOption.put(Keys.OBJECT_ID, cityStatId);
cityArticleCntOption.put(Option.OPTION_VALUE, 1);
cityArticleCntOption.put(Option.OPTION_CATEGORY, city + "-statistic");
optionRepository.add(cityArticleCntOption);
} else {
final int cityArticleCnt = cityArticleCntOption.optInt(Option.OPTION_VALUE);
cityArticleCntOption.put(Option.OPTION_VALUE, cityArticleCnt + 1);
optionRepository.update(cityStatId, cityArticleCntOption);
}
}
author.put(UserExt.USER_ARTICLE_COUNT, author.optInt(UserExt.USER_ARTICLE_COUNT) + 1);
author.put(UserExt.USER_LATEST_ARTICLE_TIME, currentTimeMillis);
// Updates user article count (and new tag count), latest article time
userRepository.update(author.optString(Keys.OBJECT_ID), author);
final String articleId = articleRepository.add(article);
if (Article.ARTICLE_TYPE_C_THOUGHT != articleType) {
// Revision
final JSONObject revision = new JSONObject();
revision.put(Revision.REVISION_AUTHOR_ID, authorId);
final JSONObject revisionData = new JSONObject();
revisionData.put(Article.ARTICLE_TITLE, articleTitle);
revisionData.put(Article.ARTICLE_CONTENT, articleContent);
revision.put(Revision.REVISION_DATA, revisionData.toString());
revision.put(Revision.REVISION_DATA_ID, articleId);
revision.put(Revision.REVISION_DATA_TYPE, Revision.DATA_TYPE_C_ARTICLE);
revisionRepository.add(revision);
}
transaction.commit();
try {
Thread.sleep(50); // wait for db write to avoid artitle duplication
} catch (final Exception e) {
}
// Grows the tag graph
tagMgmtService.relateTags(article.optString(Article.ARTICLE_TAGS));
if (!fromClient && Article.ARTICLE_ANONYMOUS_C_PUBLIC == articleAnonymous) {
// Point
final long followerCnt = followQueryService.getFollowerCount(authorId, Follow.FOLLOWING_TYPE_C_USER);
final int addition = (int) Math.round(Math.sqrt(followerCnt));
pointtransferMgmtService.transfer(authorId, Pointtransfer.ID_C_SYS,
Pointtransfer.TRANSFER_TYPE_C_ADD_ARTICLE,
Pointtransfer.TRANSFER_SUM_C_ADD_ARTICLE + addition, articleId, System.currentTimeMillis());
if (rewardPoint > 0) { // Enabe reward
pointtransferMgmtService.transfer(authorId, Pointtransfer.ID_C_SYS,
Pointtransfer.TRANSFER_TYPE_C_ADD_ARTICLE_REWARD,
Pointtransfer.TRANSFER_SUM_C_ADD_ARTICLE_REWARD, articleId, System.currentTimeMillis());
}
if (Article.ARTICLE_TYPE_C_CITY_BROADCAST == articleType) {
pointtransferMgmtService.transfer(authorId, Pointtransfer.ID_C_SYS,
Pointtransfer.TRANSFER_TYPE_C_ADD_ARTICLE_BROADCAST,
Pointtransfer.TRANSFER_SUM_C_ADD_ARTICLE_BROADCAST, articleId, System.currentTimeMillis());
}
// Liveness
livenessMgmtService.incLiveness(authorId, Liveness.LIVENESS_ARTICLE);
}
// Event
final JSONObject eventData = new JSONObject();
eventData.put(Common.FROM_CLIENT, fromClient);
eventData.put(Article.ARTICLE, article);
try {
eventManager.fireEventAsynchronously(new Event<>(EventTypes.ADD_ARTICLE, eventData));
} catch (final EventException e) {
LOGGER.log(Level.ERROR, e.getMessage(), e);
}
return ret;
} catch (final RepositoryException e) {
if (transaction.isActive()) {
transaction.rollback();
}
LOGGER.log(Level.ERROR, "Adds an article failed", e);
throw new ServiceException(e);
}
}
/**
* Updates an article with the specified request json object.
*
* @param requestJSONObject the specified request json object, for example,
* "oId": "",
* "articleTitle": "",
* "articleTags": "",
* "articleContent": "",
* "articleEditorType": "",
* "articleCommentable": boolean, // optional, default to true
* "clientArticlePermalink": "", // optional
* "articleType": int // optional, default to 0
* "articleRewardContent": "", // optional, default to ""
* "articleRewardPoint": int, // optional, default to 0
* "articleIP": "", // optional, default to ""
* "articleUA": "", // optional default to ""
* , see {@link Article} for more details
* @throws ServiceException service exception
*/
public synchronized void updateArticle(final JSONObject requestJSONObject) throws ServiceException {
String articleTitle = requestJSONObject.optString(Article.ARTICLE_TITLE);
final boolean fromClient = requestJSONObject.has(Article.ARTICLE_CLIENT_ARTICLE_ID);
String articleId;
JSONObject oldArticle;
String authorId;
JSONObject author;
int updatePointSum;
int articleAnonymous = 0;
try {
// check if admin allow to add article
final JSONObject option = optionRepository.get(Option.ID_C_MISC_ALLOW_ADD_ARTICLE);
if (!"0".equals(option.optString(Option.OPTION_VALUE))) {
throw new ServiceException(langPropsService.get("notAllowAddArticleLabel"));
}
articleId = requestJSONObject.optString(Keys.OBJECT_ID);
oldArticle = articleRepository.get(articleId);
authorId = oldArticle.optString(Article.ARTICLE_AUTHOR_ID);
author = userRepository.get(authorId);
final long followerCnt = followQueryService.getFollowerCount(authorId, Follow.FOLLOWING_TYPE_C_USER);
int addition = (int) Math.round(Math.sqrt(followerCnt));
final long collectCnt = followQueryService.getFollowerCount(articleId, Follow.FOLLOWING_TYPE_C_ARTICLE);
final long watchCnt = followQueryService.getFollowerCount(articleId, Follow.FOLLOWING_TYPE_C_ARTICLE_WATCH);
addition += (collectCnt + watchCnt) * 2;
updatePointSum = Pointtransfer.TRANSFER_SUM_C_UPDATE_ARTICLE + addition;
articleAnonymous = oldArticle.optInt(Article.ARTICLE_ANONYMOUS);
if (!fromClient && Article.ARTICLE_ANONYMOUS_C_PUBLIC == articleAnonymous) {
// Point
final int balance = author.optInt(UserExt.USER_POINT);
if (balance - updatePointSum < 0) {
throw new ServiceException(langPropsService.get("insufficientBalanceLabel"));
}
}
final JSONObject maybeExist = articleRepository.getByTitle(articleTitle);
if (null != maybeExist) {
if (!oldArticle.optString(Article.ARTICLE_TITLE).equals(articleTitle)) {
final String existArticleAuthorId = maybeExist.optString(Article.ARTICLE_AUTHOR_ID);
String msg;
if (existArticleAuthorId.equals(authorId)) {
msg = langPropsService.get("duplicatedArticleTitleSelfLabel");
msg = msg.replace("{article}", "<a target='_blank' href='/article/" + maybeExist.optString(Keys.OBJECT_ID)
+ "'>" + articleTitle + "</a>");
} else {
final JSONObject existArticleAuthor = userRepository.get(existArticleAuthorId);
final String userName = existArticleAuthor.optString(User.USER_NAME);
msg = langPropsService.get("duplicatedArticleTitleLabel");
msg = msg.replace("{user}", "<a target='_blank' href='/member/" + userName + "'>" + userName + "</a>");
msg = msg.replace("{article}", "<a target='_blank' href='/article/" + maybeExist.optString(Keys.OBJECT_ID)
+ "'>" + articleTitle + "</a>");
}
throw new ServiceException(msg);
}
}
} catch (final RepositoryException e) {
throw new ServiceException(e);
}
final int articleType = requestJSONObject.optInt(Article.ARTICLE_TYPE, Article.ARTICLE_TYPE_C_NORMAL);
final Transaction transaction = articleRepository.beginTransaction();
try {
requestJSONObject.put(Article.ARTICLE_ANONYMOUS, articleAnonymous);
processTagsForArticleUpdate(oldArticle, requestJSONObject, author);
userRepository.update(author.optString(Keys.OBJECT_ID), author);
articleTitle = Emotions.toAliases(articleTitle);
articleTitle = Pangu.spacingText(articleTitle);
final String oldTitle = oldArticle.optString(Article.ARTICLE_TITLE);
oldArticle.put(Article.ARTICLE_TITLE, articleTitle);
oldArticle.put(Article.ARTICLE_TAGS, requestJSONObject.optString(Article.ARTICLE_TAGS));
oldArticle.put(Article.ARTICLE_COMMENTABLE, requestJSONObject.optBoolean(Article.ARTICLE_COMMENTABLE, true));
oldArticle.put(Article.ARTICLE_TYPE, articleType);
String articleContent = requestJSONObject.optString(Article.ARTICLE_CONTENT);
articleContent = Emotions.toAliases(articleContent);
//articleContent = StringUtils.trim(articleContent) + " "; https://github.com/b3log/symphony/issues/389
articleContent = articleContent.replace(langPropsService.get("uploadingLabel", Locale.SIMPLIFIED_CHINESE), "");
articleContent = articleContent.replace(langPropsService.get("uploadingLabel", Locale.US), "");
final String oldContent = oldArticle.optString(Article.ARTICLE_CONTENT);
oldArticle.put(Article.ARTICLE_CONTENT, articleContent);
final long currentTimeMillis = System.currentTimeMillis();
final long createTime = oldArticle.optLong(Keys.OBJECT_ID);
oldArticle.put(Article.ARTICLE_UPDATE_TIME, currentTimeMillis);
final int rewardPoint = requestJSONObject.optInt(Article.ARTICLE_REWARD_POINT, 0);
boolean enableReward = false;
if (0 < rewardPoint) {
if (1 > oldArticle.optInt(Article.ARTICLE_REWARD_POINT)) {
enableReward = true;
}
oldArticle.put(Article.ARTICLE_REWARD_CONTENT, requestJSONObject.optString(Article.ARTICLE_REWARD_CONTENT));
oldArticle.put(Article.ARTICLE_REWARD_POINT, rewardPoint);
}
final String ip = requestJSONObject.optString(Article.ARTICLE_IP);
oldArticle.put(Article.ARTICLE_IP, ip);
String ua = requestJSONObject.optString(Article.ARTICLE_UA);
if (StringUtils.length(ua) > Common.MAX_LENGTH_UA) {
ua = StringUtils.substring(ua, 0, Common.MAX_LENGTH_UA);
}
oldArticle.put(Article.ARTICLE_UA, ua);
final String clientArticlePermalink = requestJSONObject.optString(Article.ARTICLE_CLIENT_ARTICLE_PERMALINK);
oldArticle.put(Article.ARTICLE_CLIENT_ARTICLE_PERMALINK, clientArticlePermalink);
articleRepository.update(articleId, oldArticle);
if (Article.ARTICLE_TYPE_C_THOUGHT != articleType
&& (!oldContent.equals(articleContent) || !oldTitle.equals(articleTitle))) {
// Revision
final JSONObject revision = new JSONObject();
revision.put(Revision.REVISION_AUTHOR_ID, authorId);
final JSONObject revisionData = new JSONObject();
revisionData.put(Article.ARTICLE_TITLE, articleTitle);
revisionData.put(Article.ARTICLE_CONTENT, articleContent);
revision.put(Revision.REVISION_DATA, revisionData.toString());
revision.put(Revision.REVISION_DATA_ID, articleId);
revision.put(Revision.REVISION_DATA_TYPE, Revision.DATA_TYPE_C_ARTICLE);
revisionRepository.add(revision);
}
transaction.commit();
try {
Thread.sleep(50); // wait for db write to avoid artitle duplication
} catch (final Exception e) {
}
if (!fromClient && Article.ARTICLE_ANONYMOUS_C_PUBLIC == articleAnonymous) {
if (currentTimeMillis - createTime > 1000 * 60 * 5) {
pointtransferMgmtService.transfer(authorId, Pointtransfer.ID_C_SYS,
Pointtransfer.TRANSFER_TYPE_C_UPDATE_ARTICLE,
updatePointSum, articleId, System.currentTimeMillis());
}
if (enableReward) {
pointtransferMgmtService.transfer(authorId, Pointtransfer.ID_C_SYS,
Pointtransfer.TRANSFER_TYPE_C_ADD_ARTICLE_REWARD,
Pointtransfer.TRANSFER_SUM_C_ADD_ARTICLE_REWARD, articleId, System.currentTimeMillis());
}
}
// Event
final JSONObject eventData = new JSONObject();
eventData.put(Common.FROM_CLIENT, fromClient);
eventData.put(Article.ARTICLE, oldArticle);
try {
eventManager.fireEventAsynchronously(new Event<>(EventTypes.UPDATE_ARTICLE, eventData));
} catch (final EventException e) {
LOGGER.log(Level.ERROR, e.getMessage(), e);
}
} catch (final Exception e) {
if (transaction.isActive()) {
transaction.rollback();
}
LOGGER.log(Level.ERROR, "Updates an article failed", e);
throw new ServiceException(e);
}
}
/**
* Updates the specified article by the given article id.
* <p>
* <b>Note</b>: This method just for admin console.
* </p>
*
* @param articleId the given article id
* @param article the specified article
* @throws ServiceException service exception
*/
public void updateArticleByAdmin(final String articleId, final JSONObject article) throws ServiceException {
final Transaction transaction = articleRepository.beginTransaction();
try {
final String authorId = article.optString(Article.ARTICLE_AUTHOR_ID);
final JSONObject author = userRepository.get(authorId);
article.put(Article.ARTICLE_COMMENTABLE, Boolean.valueOf(article.optBoolean(Article.ARTICLE_COMMENTABLE)));
article.put(Article.ARTICLE_SYNC_TO_CLIENT, author.optBoolean(UserExt.SYNC_TO_CLIENT));
final JSONObject oldArticle = articleRepository.get(articleId);
if (Article.ARTICLE_STATUS_C_INVALID == article.optInt(Article.ARTICLE_STATUS)) {
article.put(Article.ARTICLE_TAGS, "回收站");
}
processTagsForArticleUpdate(oldArticle, article, author);
String articleTitle = article.optString(Article.ARTICLE_TITLE);
articleTitle = Emotions.toAliases(articleTitle);
article.put(Article.ARTICLE_TITLE, articleTitle);
if (Article.ARTICLE_TYPE_C_THOUGHT == article.optInt(Article.ARTICLE_TYPE)) {
article.put(Article.ARTICLE_CONTENT, oldArticle.optString(Article.ARTICLE_CONTENT));
} else {
String articleContent = article.optString(Article.ARTICLE_CONTENT);
articleContent = Emotions.toAliases(articleContent);
article.put(Article.ARTICLE_CONTENT, articleContent);
}
final int perfect = article.optInt(Article.ARTICLE_PERFECT);
if (Article.ARTICLE_PERFECT_C_PERFECT == perfect) {
// if it is perfect, allow anonymous view
article.put(Article.ARTICLE_ANONYMOUS_VIEW, Article.ARTICLE_ANONYMOUS_VIEW_C_ALLOW);
// updates tag-article perfect
final List<JSONObject> tagArticleRels = tagArticleRepository.getByArticleId(articleId);
for (final JSONObject tagArticleRel : tagArticleRels) {
tagArticleRel.put(Article.ARTICLE_PERFECT, Article.ARTICLE_PERFECT_C_PERFECT);
tagArticleRepository.update(tagArticleRel.optString(Keys.OBJECT_ID), tagArticleRel);
}
}
userRepository.update(authorId, author);
articleRepository.update(articleId, article);
transaction.commit();
if (Article.ARTICLE_PERFECT_C_NOT_PERFECT == oldArticle.optInt(Article.ARTICLE_PERFECT)
&& Article.ARTICLE_PERFECT_C_PERFECT == perfect) {
final JSONObject notification = new JSONObject();
notification.put(Notification.NOTIFICATION_USER_ID, authorId);
notification.put(Notification.NOTIFICATION_DATA_ID, articleId);
notificationMgmtService.addPerfectArticleNotification(notification);
pointtransferMgmtService.transfer(Pointtransfer.ID_C_SYS, authorId,
Pointtransfer.TRANSFER_TYPE_C_PERFECT_ARTICLE, Pointtransfer.TRANSFER_SUM_C_PERFECT_ARTICLE,
articleId, System.currentTimeMillis());
}
if (Article.ARTICLE_STATUS_C_VALID != article.optInt(Article.ARTICLE_STATUS)) {
if (Symphonys.getBoolean("algolia.enabled")) {
searchMgmtService.removeAlgoliaDocument(article);
}
if (Symphonys.getBoolean("es.enabled")) {
searchMgmtService.removeESDocument(article, Article.ARTICLE);
}
}
} catch (final Exception e) {
if (transaction.isActive()) {
transaction.rollback();
}
LOGGER.log(Level.ERROR, "Updates an article[id=" + articleId + "] failed", e);
throw new ServiceException(e);
}
}
/**
* A user specified by the given sender id rewards the author of an article specified by the given article id.
*
* @param articleId the given article id
* @param senderId the given sender id
* @throws ServiceException service exception
*/
public void reward(final String articleId, final String senderId) throws ServiceException {
try {
final JSONObject article = articleRepository.get(articleId);
if (null == article) {
return;
}
if (Article.ARTICLE_STATUS_C_INVALID == article.optInt(Article.ARTICLE_STATUS)) {
return;
}
final JSONObject sender = userRepository.get(senderId);
if (null == sender) {
return;
}
if (UserExt.USER_STATUS_C_VALID != sender.optInt(UserExt.USER_STATUS)) {
return;
}
final String receiverId = article.optString(Article.ARTICLE_AUTHOR_ID);
final JSONObject receiver = userRepository.get(receiverId);
if (null == receiver) {
return;
}
if (UserExt.USER_STATUS_C_VALID != receiver.optInt(UserExt.USER_STATUS)) {
return;
}
if (receiverId.equals(senderId)) {
return;
}
final int rewardPoint = article.optInt(Article.ARTICLE_REWARD_POINT);
if (rewardPoint < 1) {
return;
}
if (rewardQueryService.isRewarded(senderId, articleId, Reward.TYPE_C_ARTICLE)) {
return;
}
final String rewardId = Ids.genTimeMillisId();
if (Article.ARTICLE_ANONYMOUS_C_PUBLIC == article.optInt(Article.ARTICLE_ANONYMOUS)) {
final boolean succ = null != pointtransferMgmtService.transfer(senderId, receiverId,
Pointtransfer.TRANSFER_TYPE_C_ARTICLE_REWARD, rewardPoint, rewardId, System.currentTimeMillis());
if (!succ) {
throw new ServiceException();
}
}
final JSONObject reward = new JSONObject();
reward.put(Keys.OBJECT_ID, rewardId);
reward.put(Reward.SENDER_ID, senderId);
reward.put(Reward.DATA_ID, articleId);
reward.put(Reward.TYPE, Reward.TYPE_C_ARTICLE);
rewardMgmtService.addReward(reward);
final JSONObject notification = new JSONObject();
notification.put(Notification.NOTIFICATION_USER_ID, receiverId);
notification.put(Notification.NOTIFICATION_DATA_ID, rewardId);
notificationMgmtService.addArticleRewardNotification(notification);
livenessMgmtService.incLiveness(senderId, Liveness.LIVENESS_REWARD);
} catch (final RepositoryException e) {
LOGGER.log(Level.ERROR, "Rewards an article[id=" + articleId + "] failed", e);
throw new ServiceException(e);
}
}
/**
* A user specified by the given sender id thanks the author of an article specified by the given article id.
*
* @param articleId the given article id
* @param senderId the given sender id
* @throws ServiceException service exception
*/
public void thank(final String articleId, final String senderId) throws ServiceException {
try {
final JSONObject article = articleRepository.get(articleId);
if (null == article) {
return;
}
if (Article.ARTICLE_STATUS_C_INVALID == article.optInt(Article.ARTICLE_STATUS)) {
return;
}
final JSONObject sender = userRepository.get(senderId);
if (null == sender) {
return;
}
if (UserExt.USER_STATUS_C_VALID != sender.optInt(UserExt.USER_STATUS)) {
return;
}
final String receiverId = article.optString(Article.ARTICLE_AUTHOR_ID);
final JSONObject receiver = userRepository.get(receiverId);
if (null == receiver) {
return;
}
if (UserExt.USER_STATUS_C_VALID != receiver.optInt(UserExt.USER_STATUS)) {
return;
}
if (receiverId.equals(senderId)) {
return;
}
if (rewardQueryService.isRewarded(senderId, articleId, Reward.TYPE_C_THANK_ARTICLE)) {
return;
}
final String thankId = Ids.genTimeMillisId();
if (Article.ARTICLE_ANONYMOUS_C_PUBLIC == article.optInt(Article.ARTICLE_ANONYMOUS)) {
final boolean succ = null != pointtransferMgmtService.transfer(senderId, receiverId,
Pointtransfer.TRANSFER_TYPE_C_ARTICLE_THANK,
Pointtransfer.TRANSFER_SUM_C_ARTICLE_THANK, thankId, System.currentTimeMillis());
if (!succ) {
throw new ServiceException();
}
}
final JSONObject reward = new JSONObject();
reward.put(Keys.OBJECT_ID, thankId);
reward.put(Reward.SENDER_ID, senderId);
reward.put(Reward.DATA_ID, articleId);
reward.put(Reward.TYPE, Reward.TYPE_C_THANK_ARTICLE);
rewardMgmtService.addReward(reward);
final JSONObject notification = new JSONObject();
notification.put(Notification.NOTIFICATION_USER_ID, receiverId);
notification.put(Notification.NOTIFICATION_DATA_ID, thankId);
notificationMgmtService.addArticleThankNotification(notification);
livenessMgmtService.incLiveness(senderId, Liveness.LIVENESS_REWARD);
} catch (final RepositoryException e) {
LOGGER.log(Level.ERROR, "Thanks an article[id=" + articleId + "] failed", e);
throw new ServiceException(e);
}
}
/**
* Sticks an article specified by the given article id.
*
* @param articleId the given article id
* @throws ServiceException service exception
*/
public synchronized void stick(final String articleId) throws ServiceException {
final Transaction transaction = articleRepository.beginTransaction();
try {
final JSONObject article = articleRepository.get(articleId);
if (null == article) {
return;
}
final String authorId = article.optString(Article.ARTICLE_AUTHOR_ID);
final JSONObject author = userRepository.get(authorId);
final int balance = author.optInt(UserExt.USER_POINT);
if (balance - Pointtransfer.TRANSFER_SUM_C_STICK_ARTICLE < 0) {
throw new ServiceException(langPropsService.get("insufficientBalanceLabel"));
}
final Query query = new Query().
setFilter(new PropertyFilter(Article.ARTICLE_STICK, FilterOperator.GREATER_THAN, 0L));
final JSONArray articles = articleRepository.get(query).optJSONArray(Keys.RESULTS);
if (articles.length() > 1) {
final Set<String> ids = new HashSet<>();
for (int i = 0; i < articles.length(); i++) {
ids.add(articles.optJSONObject(i).optString(Keys.OBJECT_ID));
}
if (!ids.contains(articleId)) {
throw new ServiceException(langPropsService.get("stickExistLabel"));
}
}
article.put(Article.ARTICLE_STICK, System.currentTimeMillis());
articleRepository.update(articleId, article);
transaction.commit();
final boolean succ = null != pointtransferMgmtService.transfer(article.optString(Article.ARTICLE_AUTHOR_ID),
Pointtransfer.ID_C_SYS, Pointtransfer.TRANSFER_TYPE_C_STICK_ARTICLE,
Pointtransfer.TRANSFER_SUM_C_STICK_ARTICLE, articleId, System.currentTimeMillis());
if (!succ) {
throw new ServiceException(langPropsService.get("stickFailedLabel"));
}
} catch (final RepositoryException e) {
if (transaction.isActive()) {
transaction.rollback();
}
LOGGER.log(Level.ERROR, "Sticks an article[id=" + articleId + "] failed", e);
throw new ServiceException(langPropsService.get("stickFailedLabel"));
}
}
/**
* Admin sticks an article specified by the given article id.
*
* @param articleId the given article id
* @throws ServiceException service exception
*/
@Transactional
public synchronized void adminStick(final String articleId) throws ServiceException {
try {
final JSONObject article = articleRepository.get(articleId);
if (null == article) {
return;
}
article.put(Article.ARTICLE_STICK, Long.MAX_VALUE);
articleRepository.update(articleId, article);
} catch (final RepositoryException e) {
LOGGER.log(Level.ERROR, "Admin sticks an article[id=" + articleId + "] failed", e);
throw new ServiceException(langPropsService.get("stickFailedLabel"));
}
}
/**
* Admin cancels stick an article specified by the given article id.
*
* @param articleId the given article id
* @throws ServiceException service exception
*/
@Transactional
public synchronized void adminCancelStick(final String articleId) throws ServiceException {
try {
final JSONObject article = articleRepository.get(articleId);
if (null == article) {
return;
}
article.put(Article.ARTICLE_STICK, 0L);
articleRepository.update(articleId, article);
} catch (final RepositoryException e) {
LOGGER.log(Level.ERROR, "Admin cancel sticks an article[id=" + articleId + "] failed", e);
throw new ServiceException(langPropsService.get("operationFailedLabel"));
}
}
/**
* Expires sticked articles.
*
* @throws ServiceException service exception
*/
@Transactional
public void expireStick() throws ServiceException {
try {
final Query query = new Query().
setFilter(new PropertyFilter(Article.ARTICLE_STICK, FilterOperator.GREATER_THAN, 0L));
final JSONArray articles = articleRepository.get(query).optJSONArray(Keys.RESULTS);
if (articles.length() < 1) {
return;
}
final long stepTime = Symphonys.getLong("stickArticleTime");
final long now = System.currentTimeMillis();
for (int i = 0; i < articles.length(); i++) {
final JSONObject article = articles.optJSONObject(i);
final long stick = article.optLong(Article.ARTICLE_STICK);
if (stick >= Long.MAX_VALUE) {
continue; // Skip admin stick
}
final long expired = stick + stepTime;
if (expired < now) {
article.put(Article.ARTICLE_STICK, 0L);
articleRepository.update(article.optString(Keys.OBJECT_ID), article);
}
}
} catch (final RepositoryException e) {
LOGGER.log(Level.ERROR, "Expires sticked articles failed", e);
throw new ServiceException();
}
}
/**
* Processes tags for article update.
* <p>
* <ul>
* <li>Un-tags old article, decrements tag reference count</li>
* <li>Removes old article-tag relations</li>
* <li>Saves new article-tag relations with tag reference count</li>
* </ul>
* </p>
*
* @param oldArticle the specified old article
* @param newArticle the specified new article
* @param author the specified author
* @throws Exception exception
*/
private synchronized void processTagsForArticleUpdate(final JSONObject oldArticle, final JSONObject newArticle,
final JSONObject author) throws Exception {
final String oldArticleId = oldArticle.getString(Keys.OBJECT_ID);
final List<JSONObject> oldTags = tagRepository.getByArticleId(oldArticleId);
String tagsString = newArticle.getString(Article.ARTICLE_TAGS);
tagsString = Tag.formatTags(tagsString);
boolean sandboxEnv = false;
if (StringUtils.containsIgnoreCase(tagsString, Tag.TAG_TITLE_C_SANDBOX)) {
tagsString = Tag.TAG_TITLE_C_SANDBOX;
sandboxEnv = true;
}
String[] tagStrings = tagsString.split(",");
final int articleType = newArticle.optInt(Article.ARTICLE_TYPE);
if (!sandboxEnv && tagStrings.length < TAG_MAX_CNT && tagStrings.length < 3
&& Article.ARTICLE_TYPE_C_DISCUSSION != articleType
&& Article.ARTICLE_TYPE_C_THOUGHT != articleType && !Tag.containsReservedTags(tagsString)) {
final String content = newArticle.optString(Article.ARTICLE_TITLE)
+ " " + Jsoup.parse("<p>" + newArticle.optString(Article.ARTICLE_CONTENT) + "</p>").text();
final List<String> genTags = tagQueryService.generateTags(content, 1);
if (!genTags.isEmpty()) {
tagsString = tagsString + "," + StringUtils.join(genTags, ",");
tagsString = Tag.formatTags(tagsString);
tagsString = Tag.useHead(tagsString, TAG_MAX_CNT);
}
}
if (StringUtils.isBlank(tagsString)) {
tagsString = "B3log";
}
tagsString = Tag.formatTags(tagsString);
newArticle.put(Article.ARTICLE_TAGS, tagsString);
tagStrings = tagsString.split(",");
final List<JSONObject> newTags = new ArrayList<>();
for (final String tagString : tagStrings) {
final String tagTitle = tagString.trim();
JSONObject newTag = tagRepository.getByTitle(tagTitle);
if (null == newTag) {
newTag = new JSONObject();
newTag.put(Tag.TAG_TITLE, tagTitle);
}
newTags.add(newTag);
}
final List<JSONObject> tagsDropped = new ArrayList<>();
final List<JSONObject> tagsNeedToAdd = new ArrayList<>();
for (final JSONObject newTag : newTags) {
final String newTagTitle = newTag.getString(Tag.TAG_TITLE);
if (!tagExists(newTagTitle, oldTags)) {
LOGGER.log(Level.DEBUG, "Tag need to add[title={0}]", newTagTitle);
tagsNeedToAdd.add(newTag);
}
}
for (final JSONObject oldTag : oldTags) {
final String oldTagTitle = oldTag.getString(Tag.TAG_TITLE);
if (!tagExists(oldTagTitle, newTags)) {
LOGGER.log(Level.DEBUG, "Tag dropped[title={0}]", oldTag);
tagsDropped.add(oldTag);
}
}
final int articleCmtCnt = oldArticle.getInt(Article.ARTICLE_COMMENT_CNT);
for (final JSONObject tagDropped : tagsDropped) {
final String tagId = tagDropped.getString(Keys.OBJECT_ID);
int refCnt = tagDropped.getInt(Tag.TAG_REFERENCE_CNT) - 1;
refCnt = refCnt < 0 ? 0 : refCnt;
tagDropped.put(Tag.TAG_REFERENCE_CNT, refCnt);
final int tagCmtCnt = tagDropped.getInt(Tag.TAG_COMMENT_CNT);
tagDropped.put(Tag.TAG_COMMENT_CNT, tagCmtCnt - articleCmtCnt);
tagDropped.put(Tag.TAG_RANDOM_DOUBLE, Math.random());
tagRepository.update(tagId, tagDropped);
}
final String[] tagIdsDropped = new String[tagsDropped.size()];
for (int i = 0; i < tagIdsDropped.length; i++) {
final JSONObject tag = tagsDropped.get(i);
final String id = tag.getString(Keys.OBJECT_ID);
tagIdsDropped[i] = id;
}
if (0 != tagIdsDropped.length) {
removeTagArticleRelations(oldArticleId, tagIdsDropped);
removeUserTagRelations(oldArticle.optString(Article.ARTICLE_AUTHOR_ID), Tag.TAG_TYPE_C_ARTICLE, tagIdsDropped);
}
tagStrings = new String[tagsNeedToAdd.size()];
for (int i = 0; i < tagStrings.length; i++) {
final JSONObject tag = tagsNeedToAdd.get(i);
final String tagTitle = tag.getString(Tag.TAG_TITLE);
tagStrings[i] = tagTitle;
}
newArticle.put(Article.ARTICLE_COMMENT_CNT, articleCmtCnt);
tag(tagStrings, newArticle, author);
}
/**
* Removes tag-article relations by the specified article id and tag ids of the relations to be removed.
* <p>
* Removes all relations if not specified the tag ids.
* </p>
*
* @param articleId the specified article id
* @param tagIds the specified tag ids of the relations to be removed
* @throws JSONException json exception
* @throws RepositoryException repository exception
*/
private void removeTagArticleRelations(final String articleId, final String... tagIds)
throws JSONException, RepositoryException {
final List<String> tagIdList = Arrays.asList(tagIds);
final List<JSONObject> tagArticleRelations = tagArticleRepository.getByArticleId(articleId);
for (int i = 0; i < tagArticleRelations.size(); i++) {
final JSONObject tagArticleRelation = tagArticleRelations.get(i);
String relationId;
if (tagIdList.isEmpty()) { // Removes all if un-specified
relationId = tagArticleRelation.getString(Keys.OBJECT_ID);
tagArticleRepository.remove(relationId);
} else if (tagIdList.contains(tagArticleRelation.getString(Tag.TAG + "_" + Keys.OBJECT_ID))) {
relationId = tagArticleRelation.getString(Keys.OBJECT_ID);
tagArticleRepository.remove(relationId);
}
}
}
/**
* Removes User-Tag relations by the specified user id, type and tag ids of the relations to be removed.
*
* @param userId the specified article id
* @param type the specified type
* @param tagIds the specified tag ids of the relations to be removed
* @throws RepositoryException repository exception
*/
private void removeUserTagRelations(final String userId, final int type, final String... tagIds) throws RepositoryException {
for (final String tagId : tagIds) {
userTagRepository.removeByUserIdAndTagId(userId, tagId, type);
}
}
/**
* Tags the specified article with the specified tag titles.
*
* @param tagTitles the specified (new) tag titles
* @param article the specified article
* @param author the specified author
* @throws RepositoryException repository exception
*/
private synchronized void tag(final String[] tagTitles, final JSONObject article, final JSONObject author)
throws RepositoryException {
String articleTags = article.optString(Article.ARTICLE_TAGS);
for (final String t : tagTitles) {
final String tagTitle = t.trim();
JSONObject tag = tagRepository.getByTitle(tagTitle);
String tagId;
int userTagType;
final int articleCmtCnt = article.optInt(Article.ARTICLE_COMMENT_CNT);
if (null == tag) {
LOGGER.log(Level.TRACE, "Found a new tag[title={0}] in article[title={1}]",
new Object[]{tagTitle, article.optString(Article.ARTICLE_TITLE)});
tag = new JSONObject();
tag.put(Tag.TAG_TITLE, tagTitle);
String tagURI = tagTitle;
try {
tagURI = URLEncoder.encode(tagTitle, "UTF-8");
} catch (final UnsupportedEncodingException e) {
LOGGER.log(Level.ERROR, "Encode tag title [" + tagTitle + "] error", e);
}
tag.put(Tag.TAG_URI, tagURI);
tag.put(Tag.TAG_CSS, "");
tag.put(Tag.TAG_REFERENCE_CNT, 1);
tag.put(Tag.TAG_COMMENT_CNT, articleCmtCnt);
tag.put(Tag.TAG_FOLLOWER_CNT, 0);
tag.put(Tag.TAG_LINK_CNT, 0);
tag.put(Tag.TAG_DESCRIPTION, "");
tag.put(Tag.TAG_ICON_PATH, "");
tag.put(Tag.TAG_STATUS, 0);
tag.put(Tag.TAG_GOOD_CNT, 0);
tag.put(Tag.TAG_BAD_CNT, 0);
tag.put(Tag.TAG_SEO_TITLE, tagTitle);
tag.put(Tag.TAG_SEO_KEYWORDS, tagTitle);
tag.put(Tag.TAG_SEO_DESC, "");
tag.put(Tag.TAG_RANDOM_DOUBLE, Math.random());
tagId = tagRepository.add(tag);
tag.put(Keys.OBJECT_ID, tagId);
userTagType = Tag.TAG_TYPE_C_CREATOR;
final JSONObject tagCntOption = optionRepository.get(Option.ID_C_STATISTIC_TAG_COUNT);
final int tagCnt = tagCntOption.optInt(Option.OPTION_VALUE);
tagCntOption.put(Option.OPTION_VALUE, tagCnt + 1);
optionRepository.update(Option.ID_C_STATISTIC_TAG_COUNT, tagCntOption);
author.put(UserExt.USER_TAG_COUNT, author.optInt(UserExt.USER_TAG_COUNT) + 1);
} else {
tagId = tag.optString(Keys.OBJECT_ID);
LOGGER.log(Level.TRACE, "Found a existing tag[title={0}, id={1}] in article[title={2}]",
tag.optString(Tag.TAG_TITLE), tag.optString(Keys.OBJECT_ID), article.optString(Article.ARTICLE_TITLE));
final JSONObject tagTmp = new JSONObject();
tagTmp.put(Keys.OBJECT_ID, tagId);
final String title = tag.optString(Tag.TAG_TITLE);
tagTmp.put(Tag.TAG_TITLE, title);
tagTmp.put(Tag.TAG_COMMENT_CNT, tag.optInt(Tag.TAG_COMMENT_CNT) + articleCmtCnt);
tagTmp.put(Tag.TAG_STATUS, tag.optInt(Tag.TAG_STATUS));
tagTmp.put(Tag.TAG_REFERENCE_CNT, tag.optInt(Tag.TAG_REFERENCE_CNT) + 1);
tagTmp.put(Tag.TAG_FOLLOWER_CNT, tag.optInt(Tag.TAG_FOLLOWER_CNT));
tagTmp.put(Tag.TAG_LINK_CNT, tag.optInt(Tag.TAG_LINK_CNT));
tagTmp.put(Tag.TAG_DESCRIPTION, tag.optString(Tag.TAG_DESCRIPTION));
tagTmp.put(Tag.TAG_ICON_PATH, tag.optString(Tag.TAG_ICON_PATH));
tagTmp.put(Tag.TAG_GOOD_CNT, tag.optInt(Tag.TAG_GOOD_CNT));
tagTmp.put(Tag.TAG_BAD_CNT, tag.optInt(Tag.TAG_BAD_CNT));
tagTmp.put(Tag.TAG_SEO_DESC, tag.optString(Tag.TAG_SEO_DESC));
tagTmp.put(Tag.TAG_SEO_KEYWORDS, tag.optString(Tag.TAG_SEO_KEYWORDS));
tagTmp.put(Tag.TAG_SEO_TITLE, tag.optString(Tag.TAG_SEO_TITLE));
tagTmp.put(Tag.TAG_RANDOM_DOUBLE, Math.random());
tagTmp.put(Tag.TAG_URI, tag.optString(Tag.TAG_URI));
tagTmp.put(Tag.TAG_CSS, tag.optString(Tag.TAG_CSS));
tagRepository.update(tagId, tagTmp);
userTagType = Tag.TAG_TYPE_C_ARTICLE;
}
// Tag-Article relation
final JSONObject tagArticleRelation = new JSONObject();
tagArticleRelation.put(Tag.TAG + "_" + Keys.OBJECT_ID, tagId);
tagArticleRelation.put(Article.ARTICLE + "_" + Keys.OBJECT_ID, article.optString(Keys.OBJECT_ID));
tagArticleRelation.put(Article.ARTICLE_LATEST_CMT_TIME, article.optLong(Article.ARTICLE_LATEST_CMT_TIME));
tagArticleRelation.put(Article.ARTICLE_COMMENT_CNT, article.optInt(Article.ARTICLE_COMMENT_CNT));
tagArticleRelation.put(Article.REDDIT_SCORE, article.optDouble(Article.REDDIT_SCORE, 0D));
tagArticleRelation.put(Article.ARTICLE_PERFECT, article.optInt(Article.ARTICLE_PERFECT));
tagArticleRepository.add(tagArticleRelation);
final String authorId = article.optString(Article.ARTICLE_AUTHOR_ID);
// User-Tag relation
if (Tag.TAG_TYPE_C_ARTICLE == userTagType) {
userTagRepository.removeByUserIdAndTagId(authorId, tagId, Tag.TAG_TYPE_C_ARTICLE);
}
final JSONObject userTagRelation = new JSONObject();
userTagRelation.put(Tag.TAG + '_' + Keys.OBJECT_ID, tagId);
if (Article.ARTICLE_ANONYMOUS_C_ANONYMOUS == article.optInt(Article.ARTICLE_ANONYMOUS)) {
userTagRelation.put(User.USER + '_' + Keys.OBJECT_ID, "0");
} else {
userTagRelation.put(User.USER + '_' + Keys.OBJECT_ID, authorId);
}
userTagRelation.put(Common.TYPE, userTagType);
userTagRepository.add(userTagRelation);
}
final String[] tags = articleTags.split(",");
final StringBuilder builder = new StringBuilder();
for (final String tagTitle : tags) {
final JSONObject tag = tagRepository.getByTitle(tagTitle);
builder.append(tag.optString(Tag.TAG_TITLE)).append(",");
}
if (builder.length() > 0) {
builder.deleteCharAt(builder.length() - 1);
}
article.put(Article.ARTICLE_TAGS, builder.toString());
}
/**
* Filters the specified article tags.
*
* @param articleTags the specified article tags
* @return filtered tags string
*/
public String filterReservedTags(final String articleTags) {
final String[] tags = articleTags.split(",");
final StringBuilder retBuilder = new StringBuilder();
for (final String tag : tags) {
if (!ArrayUtils.contains(Symphonys.RESERVED_TAGS, tag)) {
retBuilder.append(tag).append(",");
}
}
if (retBuilder.length() > 0) {
retBuilder.deleteCharAt(retBuilder.length() - 1);
}
return retBuilder.toString();
}
/**
* Adds an article with the specified request json object.
* <p>
* <b>Note</b>: This method just for admin console.
* </p>
*
* @param requestJSONObject the specified request json object, for example,
* "articleTitle": "",
* "articleTags": "",
* "articleContent": "",
* "articleRewardContent": "",
* "articleRewardPoint": int,
* "userName": "",
* "time": long
* , see {@link Article} for more details
* @return generated article id
* @throws ServiceException service exception
*/
public synchronized String addArticleByAdmin(final JSONObject requestJSONObject) throws ServiceException {
JSONObject author;
try {
author = userRepository.getByName(requestJSONObject.optString(User.USER_NAME));
if (null == author) {
throw new ServiceException(langPropsService.get("notFoundUserLabel"));
}
} catch (final RepositoryException e) {
LOGGER.log(Level.DEBUG, "Admin adds article failed", e);
throw new ServiceException(e.getMessage());
}
final Transaction transaction = articleRepository.beginTransaction();
try {
final long time = requestJSONObject.optLong(Common.TIME);
final String ret = String.valueOf(time);
final JSONObject article = new JSONObject();
article.put(Keys.OBJECT_ID, ret);
article.put(Article.ARTICLE_CLIENT_ARTICLE_ID, ret);
article.put(Article.ARTICLE_CLIENT_ARTICLE_PERMALINK, "");
article.put(Article.ARTICLE_AUTHOR_ID, author.optString(Keys.OBJECT_ID));
article.put(Article.ARTICLE_TITLE, Emotions.toAliases(requestJSONObject.optString(Article.ARTICLE_TITLE)));
article.put(Article.ARTICLE_CONTENT, Emotions.toAliases(requestJSONObject.optString(Article.ARTICLE_CONTENT)));
article.put(Article.ARTICLE_REWARD_CONTENT, requestJSONObject.optString(Article.ARTICLE_REWARD_CONTENT));
article.put(Article.ARTICLE_EDITOR_TYPE, 0);
article.put(Article.ARTICLE_SYNC_TO_CLIENT, false);
article.put(Article.ARTICLE_COMMENT_CNT, 0);
article.put(Article.ARTICLE_VIEW_CNT, 0);
article.put(Article.ARTICLE_GOOD_CNT, 0);
article.put(Article.ARTICLE_BAD_CNT, 0);
article.put(Article.ARTICLE_COLLECT_CNT, 0);
article.put(Article.ARTICLE_WATCH_CNT, 0);
article.put(Article.ARTICLE_COMMENTABLE, true);
article.put(Article.ARTICLE_CREATE_TIME, time);
article.put(Article.ARTICLE_UPDATE_TIME, time);
article.put(Article.ARTICLE_LATEST_CMT_TIME, 0);
article.put(Article.ARTICLE_LATEST_CMTER_NAME, "");
article.put(Article.ARTICLE_PERMALINK, "/article/" + ret);
article.put(Article.ARTICLE_RANDOM_DOUBLE, Math.random());
article.put(Article.REDDIT_SCORE, 0);
article.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_VALID);
article.put(Article.ARTICLE_TYPE, Article.ARTICLE_TYPE_C_NORMAL);
article.put(Article.ARTICLE_REWARD_POINT, requestJSONObject.optInt(Article.ARTICLE_REWARD_POINT));
article.put(Article.ARTICLE_CITY, "");
String articleTags = requestJSONObject.optString(Article.ARTICLE_TAGS);
articleTags = Tag.formatTags(articleTags);
boolean sandboxEnv = false;
if (StringUtils.containsIgnoreCase(articleTags, Tag.TAG_TITLE_C_SANDBOX)) {
articleTags = Tag.TAG_TITLE_C_SANDBOX;
sandboxEnv = true;
}
String[] tagTitles = articleTags.split(",");
if (!sandboxEnv && tagTitles.length < TAG_MAX_CNT && tagTitles.length < 3
&& !Tag.containsReservedTags(articleTags)) {
final String content = article.optString(Article.ARTICLE_TITLE)
+ " " + Jsoup.parse("<p>" + article.optString(Article.ARTICLE_CONTENT) + "</p>").text();
final List<String> genTags = tagQueryService.generateTags(content, TAG_MAX_CNT);
if (!genTags.isEmpty()) {
articleTags = articleTags + "," + StringUtils.join(genTags, ",");
articleTags = Tag.formatTags(articleTags);
articleTags = Tag.useHead(articleTags, TAG_MAX_CNT);
}
}
if (StringUtils.isBlank(articleTags)) {
articleTags = "B3log";
}
articleTags = Tag.formatTags(articleTags);
article.put(Article.ARTICLE_TAGS, articleTags);
tagTitles = articleTags.split(",");
tag(tagTitles, article, author);
final String ip = requestJSONObject.optString(Article.ARTICLE_IP);
article.put(Article.ARTICLE_IP, ip);
String ua = requestJSONObject.optString(Article.ARTICLE_UA);
if (StringUtils.length(ua) > Common.MAX_LENGTH_UA) {
ua = StringUtils.substring(ua, 0, Common.MAX_LENGTH_UA);
}
article.put(Article.ARTICLE_UA, ua);
article.put(Article.ARTICLE_STICK, 0L);
article.put(Article.ARTICLE_ANONYMOUS, Article.ARTICLE_ANONYMOUS_C_PUBLIC);
article.put(Article.ARTICLE_PERFECT, Article.ARTICLE_PERFECT_C_NOT_PERFECT);
article.put(Article.ARTICLE_ANONYMOUS_VIEW, Article.ARTICLE_ANONYMOUS_VIEW_C_USE_GLOBAL);
article.put(Article.ARTICLE_AUDIO_URL, "");
final JSONObject articleCntOption = optionRepository.get(Option.ID_C_STATISTIC_ARTICLE_COUNT);
final int articleCnt = articleCntOption.optInt(Option.OPTION_VALUE);
articleCntOption.put(Option.OPTION_VALUE, articleCnt + 1);
optionRepository.update(Option.ID_C_STATISTIC_ARTICLE_COUNT, articleCntOption);
author.put(UserExt.USER_ARTICLE_COUNT, author.optInt(UserExt.USER_ARTICLE_COUNT) + 1);
author.put(UserExt.USER_LATEST_ARTICLE_TIME, time);
// Updates user article count (and new tag count), latest article time
userRepository.update(author.optString(Keys.OBJECT_ID), author);
final String articleId = articleRepository.add(article);
// Revision
final JSONObject revision = new JSONObject();
revision.put(Revision.REVISION_AUTHOR_ID, author.optString(Keys.OBJECT_ID));
final JSONObject revisionData = new JSONObject();
revisionData.put(Article.ARTICLE_TITLE, article.optString(Article.ARTICLE_TITLE));
revisionData.put(Article.ARTICLE_CONTENT, article.optString(Article.ARTICLE_CONTENT));
revision.put(Revision.REVISION_DATA, revisionData.toString());
revision.put(Revision.REVISION_DATA_ID, articleId);
revision.put(Revision.REVISION_DATA_TYPE, Revision.DATA_TYPE_C_ARTICLE);
revisionRepository.add(revision);
transaction.commit();
// Grows the tag graph
tagMgmtService.relateTags(article.optString(Article.ARTICLE_TAGS));
// Event
final JSONObject eventData = new JSONObject();
eventData.put(Common.FROM_CLIENT, false);
eventData.put(Article.ARTICLE, article);
try {
eventManager.fireEventAsynchronously(new Event<>(EventTypes.ADD_ARTICLE, eventData));
} catch (final EventException e) {
LOGGER.log(Level.ERROR, e.getMessage(), e);
}
return ret;
} catch (final RepositoryException e) {
if (transaction.isActive()) {
transaction.rollback();
}
LOGGER.log(Level.ERROR, "Admin adds an article failed", e);
throw new ServiceException(e.getMessage());
}
}
}