/**
   * Gets the followers of a following specified by the given following id and follow type.
   *
   * @param followingId the given following id
   * @param followingType the specified following type
   * @param currentPageNum the specified current page number
   * @param pageSize the specified page size
   * @return result json object, for example,
   *     <pre>
   * {
   *     "paginationRecordCount": int,
   *     "rslts": java.util.List[{
   *         "oId": "",
   *         "followerId": "",
   *         "followingId": "",
   *         "followingType": int
   *     }, ....]
   * }
   * </pre>
   *
   * @throws RepositoryException repository exception
   */
  private JSONObject getFollowers(
      final String followingId,
      final int followingType,
      final int currentPageNum,
      final int pageSize)
      throws RepositoryException {
    final List<Filter> filters = new ArrayList<Filter>();
    filters.add(new PropertyFilter(Follow.FOLLOWING_ID, FilterOperator.EQUAL, followingId));
    filters.add(new PropertyFilter(Follow.FOLLOWING_TYPE, FilterOperator.EQUAL, followingType));

    final Query query =
        new Query()
            .addSort(Keys.OBJECT_ID, SortDirection.DESCENDING)
            .setFilter(new CompositeFilter(CompositeFilterOperator.AND, filters))
            .setPageSize(pageSize)
            .setCurrentPageNum(currentPageNum);

    final JSONObject result = followRepository.get(query);

    final List<JSONObject> records =
        CollectionUtils.<JSONObject>jsonArrayToList(result.optJSONArray(Keys.RESULTS));
    final int recordCnt =
        result.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_RECORD_COUNT);

    final JSONObject ret = new JSONObject();
    ret.put(Keys.RESULTS, (Object) records);
    ret.put(Pagination.PAGINATION_RECORD_COUNT, recordCnt);

    return ret;
  }
  /**
   * Gets the latest comments with the specified fetch size.
   *
   * <p>The returned comments content is plain text.
   *
   * @param fetchSize the specified fetch size
   * @return the latest comments, returns an empty list if not found
   * @throws ServiceException service exception
   */
  public List<JSONObject> getLatestComments(final int fetchSize) throws ServiceException {
    final Query query =
        new Query()
            .addSort(Comment.COMMENT_CREATE_TIME, SortDirection.DESCENDING)
            .setCurrentPageNum(1)
            .setPageSize(fetchSize)
            .setPageCount(1);
    try {
      final JSONObject result = commentRepository.get(query);
      final List<JSONObject> ret =
          CollectionUtils.<JSONObject>jsonArrayToList(result.optJSONArray(Keys.RESULTS));

      for (final JSONObject comment : ret) {
        comment.put(Comment.COMMENT_CREATE_TIME, comment.optLong(Comment.COMMENT_CREATE_TIME));
        final String articleId = comment.optString(Comment.COMMENT_ON_ARTICLE_ID);
        final JSONObject article = articleRepository.get(articleId);
        comment.put(
            Comment.COMMENT_T_ARTICLE_TITLE,
            Emotions.clear(article.optString(Article.ARTICLE_TITLE)));
        comment.put(
            Comment.COMMENT_T_ARTICLE_PERMALINK, article.optString(Article.ARTICLE_PERMALINK));

        final String commenterId = comment.optString(Comment.COMMENT_AUTHOR_ID);
        final JSONObject commenter = userRepository.get(commenterId);

        if (UserExt.USER_STATUS_C_INVALID == commenter.optInt(UserExt.USER_STATUS)
            || Comment.COMMENT_STATUS_C_INVALID == comment.optInt(Comment.COMMENT_STATUS)) {
          comment.put(Comment.COMMENT_CONTENT, langPropsService.get("commentContentBlockLabel"));
        }

        if (Article.ARTICLE_TYPE_C_DISCUSSION == article.optInt(Article.ARTICLE_TYPE)) {
          comment.put(Comment.COMMENT_CONTENT, "....");
        }

        String content = comment.optString(Comment.COMMENT_CONTENT);
        content = Emotions.clear(content);
        content = Jsoup.clean(content, Whitelist.none());
        if (StringUtils.isBlank(content)) {
          comment.put(Comment.COMMENT_CONTENT, "....");
        } else {
          comment.put(Comment.COMMENT_CONTENT, content);
        }

        final String commenterEmail = comment.optString(Comment.COMMENT_AUTHOR_EMAIL);
        final String avatarURL = avatarQueryService.getAvatarURL(commenterEmail);
        commenter.put(UserExt.USER_AVATAR_URL, avatarURL);

        comment.put(Comment.COMMENT_T_COMMENTER, commenter);
      }

      return ret;
    } catch (final RepositoryException e) {
      LOGGER.log(Level.ERROR, "Gets user comments failed", e);
      throw new ServiceException(e);
    }
  }
  @Override
  public List<JSONObject> getMostUsedTags(final int num) throws RepositoryException {
    final Query query =
        new Query()
            .addSort(Tag.TAG_PUBLISHED_REFERENCE_COUNT, SortDirection.DESCENDING)
            .setCurrentPageNum(1)
            .setPageSize(num)
            .setPageCount(1);

    final JSONObject result = get(query);
    final JSONArray array = result.optJSONArray(Keys.RESULTS);

    return CollectionUtils.jsonArrayToList(array);
  }
  /**
   * Gets the article comments with the specified article id, page number and page size.
   *
   * @param articleId the specified article id
   * @param currentPageNum the specified page number
   * @param pageSize the specified page size
   * @return comments, return an empty list if not found
   * @throws ServiceException service exception
   */
  public List<JSONObject> getArticleComments(
      final String articleId, final int currentPageNum, final int pageSize)
      throws ServiceException {
    final Query query =
        new Query()
            .addSort(Comment.COMMENT_CREATE_TIME, SortDirection.DESCENDING)
            .setPageCount(1)
            .setCurrentPageNum(currentPageNum)
            .setPageSize(pageSize)
            .setFilter(
                new PropertyFilter(Comment.COMMENT_ON_ARTICLE_ID, FilterOperator.EQUAL, articleId));
    try {
      final JSONObject result = commentRepository.get(query);
      final List<JSONObject> ret =
          CollectionUtils.<JSONObject>jsonArrayToList(result.optJSONArray(Keys.RESULTS));

      organizeComments(ret);

      return ret;
    } catch (final RepositoryException e) {
      LOGGER.log(Level.ERROR, "Gets article [" + articleId + "] comments failed", e);
      throw new ServiceException(e);
    }
  }
  /**
   * Gets comments by the specified request json object.
   *
   * @param requestJSONObject the specified request json object, for example,
   *     <pre>
   * {
   *     "paginationCurrentPageNum": 1,
   *     "paginationPageSize": 20,
   *     "paginationWindowSize": 10,
   * }, see {@link Pagination} for more details
   * </pre>
   *
   * @param commentFields the specified article fields to return
   * @return for example,
   *     <pre>
   * {
   *     "pagination": {
   *         "paginationPageCount": 100,
   *         "paginationPageNums": [1, 2, 3, 4, 5]
   *     },
   *     "comments": [{
   *         "oId": "",
   *         "commentContent": "",
   *         "commentCreateTime": "",
   *         ....
   *      }, ....]
   * }
   * </pre>
   *
   * @throws ServiceException service exception
   * @see Pagination
   */
  public JSONObject getComments(
      final JSONObject requestJSONObject, final Map<String, Class<?>> commentFields)
      throws ServiceException {
    final JSONObject ret = new JSONObject();

    final int currentPageNum = requestJSONObject.optInt(Pagination.PAGINATION_CURRENT_PAGE_NUM);
    final int pageSize = requestJSONObject.optInt(Pagination.PAGINATION_PAGE_SIZE);
    final int windowSize = requestJSONObject.optInt(Pagination.PAGINATION_WINDOW_SIZE);
    final Query query =
        new Query()
            .setCurrentPageNum(currentPageNum)
            .setPageSize(pageSize)
            .addSort(Comment.COMMENT_CREATE_TIME, SortDirection.DESCENDING);
    for (final Map.Entry<String, Class<?>> commentField : commentFields.entrySet()) {
      query.addProjection(commentField.getKey(), commentField.getValue());
    }

    JSONObject result = null;

    try {
      result = commentRepository.get(query);
    } catch (final RepositoryException e) {
      LOGGER.log(Level.ERROR, "Gets comments failed", e);

      throw new ServiceException(e);
    }

    final int pageCount =
        result.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT);

    final JSONObject pagination = new JSONObject();
    ret.put(Pagination.PAGINATION, pagination);
    final List<Integer> pageNums =
        Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize);
    pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
    pagination.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);

    final JSONArray data = result.optJSONArray(Keys.RESULTS);
    final List<JSONObject> comments = CollectionUtils.<JSONObject>jsonArrayToList(data);

    try {
      for (final JSONObject comment : comments) {
        organizeComment(comment);

        final String articleId = comment.optString(Comment.COMMENT_ON_ARTICLE_ID);
        final JSONObject article = articleRepository.get(articleId);

        comment.put(
            Comment.COMMENT_T_ARTICLE_TITLE,
            Article.ARTICLE_STATUS_C_INVALID == article.optInt(Article.ARTICLE_STATUS)
                ? langPropsService.get("articleTitleBlockLabel")
                : Emotions.convert(article.optString(Article.ARTICLE_TITLE)));
        comment.put(
            Comment.COMMENT_T_ARTICLE_PERMALINK, article.optString(Article.ARTICLE_PERMALINK));
      }
    } catch (final RepositoryException e) {
      LOGGER.log(Level.ERROR, "Organizes comments failed", e);

      throw new ServiceException(e);
    }

    ret.put(Comment.COMMENTS, comments);

    return ret;
  }
  /**
   * Gets the user comments with the specified user id, page number and page size.
   *
   * @param userId the specified user id
   * @param currentPageNum the specified page number
   * @param pageSize the specified page size
   * @param viewer the specified viewer, may be {@code null}
   * @return user comments, return an empty list if not found
   * @throws ServiceException service exception
   */
  public List<JSONObject> getUserComments(
      final String userId, final int currentPageNum, final int pageSize, final JSONObject viewer)
      throws ServiceException {
    final Query query =
        new Query()
            .addSort(Comment.COMMENT_CREATE_TIME, SortDirection.DESCENDING)
            .setCurrentPageNum(currentPageNum)
            .setPageSize(pageSize)
            .setFilter(new PropertyFilter(Comment.COMMENT_AUTHOR_ID, FilterOperator.EQUAL, userId));
    try {
      final JSONObject result = commentRepository.get(query);
      final List<JSONObject> ret =
          CollectionUtils.<JSONObject>jsonArrayToList(result.optJSONArray(Keys.RESULTS));

      for (final JSONObject comment : ret) {
        comment.put(
            Comment.COMMENT_CREATE_TIME, new Date(comment.optLong(Comment.COMMENT_CREATE_TIME)));

        final String articleId = comment.optString(Comment.COMMENT_ON_ARTICLE_ID);
        final JSONObject article = articleRepository.get(articleId);

        comment.put(
            Comment.COMMENT_T_ARTICLE_TITLE,
            Article.ARTICLE_STATUS_C_INVALID == article.optInt(Article.ARTICLE_STATUS)
                ? langPropsService.get("articleTitleBlockLabel")
                : Emotions.convert(article.optString(Article.ARTICLE_TITLE)));
        comment.put(Comment.COMMENT_T_ARTICLE_TYPE, article.optInt(Article.ARTICLE_TYPE));
        comment.put(
            Comment.COMMENT_T_ARTICLE_PERMALINK, article.optString(Article.ARTICLE_PERMALINK));

        final JSONObject commenter = userRepository.get(userId);
        comment.put(Comment.COMMENT_T_COMMENTER, commenter);

        final String articleAuthorId = article.optString(Article.ARTICLE_AUTHOR_ID);
        final JSONObject articleAuthor = userRepository.get(articleAuthorId);
        final String articleAuthorName = articleAuthor.optString(User.USER_NAME);
        final String articleAuthorURL = "/member/" + articleAuthor.optString(User.USER_NAME);
        comment.put(Comment.COMMENT_T_ARTICLE_AUTHOR_NAME, articleAuthorName);
        comment.put(Comment.COMMENT_T_ARTICLE_AUTHOR_URL, articleAuthorURL);
        final String articleAuthorEmail = articleAuthor.optString(User.USER_EMAIL);
        final String articleAuthorThumbnailURL =
            avatarQueryService.getAvatarURL(articleAuthorEmail);
        comment.put(Comment.COMMENT_T_ARTICLE_AUTHOR_THUMBNAIL_URL, articleAuthorThumbnailURL);

        if (Article.ARTICLE_TYPE_C_DISCUSSION == article.optInt(Article.ARTICLE_TYPE)) {
          final String msgContent =
              langPropsService
                  .get("articleDiscussionLabel")
                  .replace(
                      "{user}",
                      "<a href='"
                          + Latkes.getServePath()
                          + "/member/"
                          + articleAuthorName
                          + "'>"
                          + articleAuthorName
                          + "</a>");

          if (null == viewer) {
            comment.put(Comment.COMMENT_CONTENT, msgContent);
          } else {
            final String commenterName = commenter.optString(User.USER_NAME);
            final String viewerUserName = viewer.optString(User.USER_NAME);
            final String viewerRole = viewer.optString(User.USER_ROLE);

            if (!commenterName.equals(viewerUserName) && !Role.ADMIN_ROLE.equals(viewerRole)) {
              final String articleContent = article.optString(Article.ARTICLE_CONTENT);
              final Set<String> userNames = userQueryService.getUserNames(articleContent);

              boolean invited = false;
              for (final String userName : userNames) {
                if (userName.equals(viewerUserName)) {
                  invited = true;

                  break;
                }
              }

              if (!invited) {
                comment.put(Comment.COMMENT_CONTENT, msgContent);
              }
            }
          }
        }

        processCommentContent(comment);
      }

      return ret;
    } catch (final RepositoryException e) {
      LOGGER.log(Level.ERROR, "Gets user comments failed", e);
      throw new ServiceException(e);
    }
  }