/* * 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()); } } }