/**
   * Shows article with the specified article id.
   *
   * @param context the specified context
   * @param request the specified request
   * @param response the specified response
   * @param articleId the specified article id
   * @throws Exception exception
   */
  @RequestProcessing(value = "/article/{articleId}", method = HTTPRequestMethod.GET)
  @Before(adviceClass = StopwatchStartAdvice.class)
  @After(adviceClass = {CSRFToken.class, StopwatchEndAdvice.class})
  public void showArticle(
      final HTTPRequestContext context,
      final HttpServletRequest request,
      final HttpServletResponse response,
      final String articleId)
      throws Exception {
    final AbstractFreeMarkerRenderer renderer = new SkinRenderer();
    context.setRenderer(renderer);

    renderer.setTemplateName("/article.ftl");
    final Map<String, Object> dataModel = renderer.getDataModel();

    final JSONObject article = articleQueryService.getArticleById(articleId);
    if (null == article) {
      response.sendError(HttpServletResponse.SC_NOT_FOUND);

      return;
    }

    final HttpSession session = request.getSession(false);
    if (null != session) {
      session.setAttribute(Article.ARTICLE_T_ID, articleId);
    }

    filler.fillHeaderAndFooter(request, response, dataModel);

    final String authorEmail = article.optString(Article.ARTICLE_AUTHOR_EMAIL);
    final JSONObject author = userQueryService.getUserByEmail(authorEmail);
    article.put(Article.ARTICLE_T_AUTHOR_NAME, author.optString(User.USER_NAME));
    article.put(Article.ARTICLE_T_AUTHOR_URL, author.optString(User.USER_URL));
    article.put(Article.ARTICLE_T_AUTHOR_INTRO, author.optString(UserExt.USER_INTRO));
    dataModel.put(Article.ARTICLE, article);

    article.put(Common.IS_MY_ARTICLE, false);
    article.put(Article.ARTICLE_T_AUTHOR, author);
    article.put(Common.REWARDED, false);

    articleQueryService.processArticleContent(article, request);

    final boolean isLoggedIn = (Boolean) dataModel.get(Common.IS_LOGGED_IN);
    JSONObject currentUser;
    String currentUserId = null;
    if (isLoggedIn) {
      currentUser = (JSONObject) dataModel.get(Common.CURRENT_USER);
      currentUserId = currentUser.optString(Keys.OBJECT_ID);

      article.put(
          Common.IS_MY_ARTICLE, currentUserId.equals(article.optString(Article.ARTICLE_AUTHOR_ID)));

      final boolean isFollowing = followQueryService.isFollowing(currentUserId, articleId);
      dataModel.put(Common.IS_FOLLOWING, isFollowing);

      final int vote = voteQueryService.isVoted(currentUserId, articleId);
      dataModel.put(Vote.VOTE, vote);

      if (currentUserId.equals(author.optString(Keys.OBJECT_ID))) {
        article.put(Common.REWARDED, true);
      } else {
        article.put(
            Common.REWARDED,
            rewardQueryService.isRewarded(currentUserId, articleId, Reward.TYPE_C_ARTICLE));
      }
    }

    if (!(Boolean) request.getAttribute(Keys.HttpRequest.IS_SEARCH_ENGINE_BOT)) {
      articleMgmtService.incArticleViewCount(articleId);
    }

    filler.fillRelevantArticles(dataModel, article);
    filler.fillRandomArticles(dataModel);
    filler.fillHotArticles(dataModel);

    // Qiniu file upload authenticate
    final Auth auth =
        Auth.create(Symphonys.get("qiniu.accessKey"), Symphonys.get("qiniu.secretKey"));
    final String uploadToken = auth.uploadToken(Symphonys.get("qiniu.bucket"));
    dataModel.put("qiniuUploadToken", uploadToken);
    dataModel.put("qiniuDomain", Symphonys.get("qiniu.domain"));

    dataModel.put(Common.DISCUSSION_VIEWABLE, article.optBoolean(Common.DISCUSSION_VIEWABLE));
    if (!article.optBoolean(Common.DISCUSSION_VIEWABLE)) {
      article.put(Article.ARTICLE_T_COMMENTS, (Object) Collections.emptyList());

      return;
    }

    String pageNumStr = request.getParameter("p");
    if (Strings.isEmptyOrNull(pageNumStr) || !Strings.isNumeric(pageNumStr)) {
      pageNumStr = "1";
    }

    final int pageNum = Integer.valueOf(pageNumStr);
    final int pageSize = Symphonys.getInt("articleCommentsPageSize");
    final int windowSize = Symphonys.getInt("articleCommentsWindowSize");

    final List<JSONObject> articleComments =
        commentQueryService.getArticleComments(articleId, pageNum, pageSize);
    article.put(Article.ARTICLE_T_COMMENTS, (Object) articleComments);

    // Fill reward(thank)
    for (final JSONObject comment : articleComments) {
      String thankTemplate = langPropsService.get("thankConfirmLabel");
      thankTemplate =
          thankTemplate
              .replace("{point}", String.valueOf(Symphonys.getInt("pointThankComment")))
              .replace(
                  "{user}",
                  comment.optJSONObject(Comment.COMMENT_T_COMMENTER).optString(User.USER_NAME));
      comment.put(Comment.COMMENT_T_THANK_LABEL, thankTemplate);

      final String commentId = comment.optString(Keys.OBJECT_ID);
      if (isLoggedIn) {
        comment.put(
            Common.REWARDED,
            rewardQueryService.isRewarded(currentUserId, commentId, Reward.TYPE_C_COMMENT));
      }

      comment.put(
          Common.REWARED_COUNT, rewardQueryService.rewardedCount(commentId, Reward.TYPE_C_COMMENT));
    }

    final int commentCnt = article.getInt(Article.ARTICLE_COMMENT_CNT);
    final int pageCount = (int) Math.ceil((double) commentCnt / (double) pageSize);

    final List<Integer> pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
    if (!pageNums.isEmpty()) {
      dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
      dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
    }

    dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
    dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
    dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
    dataModel.put(Common.ARTICLE_COMMENTS_PAGE_SIZE, pageSize);
  }
  @Override
  public void action(final Event<JSONObject> event) throws EventException {
    final JSONObject data = event.getData();
    LOGGER.log(
        Level.DEBUG,
        "Processing an event[type={0}, data={1}] in listener[className={2}]",
        new Object[] {event.getType(), data, ArticleNotifier.class.getName()});

    try {
      final JSONObject originalArticle = data.getJSONObject(Article.ARTICLE);
      final String articleId = originalArticle.optString(Keys.OBJECT_ID);

      final String articleAuthorId = originalArticle.optString(Article.ARTICLE_AUTHOR_ID);
      final JSONObject articleAuthor = userQueryService.getUser(articleAuthorId);
      final String articleAuthorName = articleAuthor.optString(User.USER_NAME);

      final String articleContent = originalArticle.optString(Article.ARTICLE_CONTENT);
      final Set<String> atUserNames = userQueryService.getUserNames(articleContent);
      atUserNames.remove(articleAuthorName); // Do not notify the author itself

      final Set<String> atedUserIds = new HashSet<String>();

      // 'At' Notification
      for (final String userName : atUserNames) {
        final JSONObject user = userQueryService.getUserByName(userName);

        if (null == user) {
          LOGGER.log(Level.WARN, "Not found user by name [{0}]", userName);

          continue;
        }

        final JSONObject requestJSONObject = new JSONObject();
        final String atedUserId = user.optString(Keys.OBJECT_ID);
        requestJSONObject.put(Notification.NOTIFICATION_USER_ID, atedUserId);
        requestJSONObject.put(Notification.NOTIFICATION_DATA_ID, articleId);

        notificationMgmtService.addAtNotification(requestJSONObject);

        atedUserIds.add(atedUserId);
      }

      // 'FollowingUser' Notification
      final JSONObject followerUsersResult =
          followQueryService.getFollowerUsers(articleAuthorId, 1, Integer.MAX_VALUE);
      @SuppressWarnings("unchecked")
      final List<JSONObject> followerUsers = (List) followerUsersResult.opt(Keys.RESULTS);
      for (final JSONObject followerUser : followerUsers) {
        final JSONObject requestJSONObject = new JSONObject();
        final String followerUserId = followerUser.optString(Keys.OBJECT_ID);

        if (atedUserIds.contains(followerUserId)) {
          continue;
        }

        requestJSONObject.put(Notification.NOTIFICATION_USER_ID, followerUserId);
        requestJSONObject.put(Notification.NOTIFICATION_DATA_ID, articleId);

        notificationMgmtService.addFollowingUserNotification(requestJSONObject);
      }

      // Timeline
      final String articleTitle =
          StringUtils.substring(
              Jsoup.parse(originalArticle.optString(Article.ARTICLE_TITLE)).text(), 0, 28);
      final String articlePermalink =
          Latkes.getServePath() + originalArticle.optString(Article.ARTICLE_PERMALINK);

      final JSONObject timeline = new JSONObject();
      timeline.put(Common.TYPE, Article.ARTICLE);
      String content = langPropsService.get("timelineArticleLabel");
      content =
          content
              .replace(
                  "{user}",
                  "<a target='_blank' rel='nofollow' href='"
                      + Latkes.getServePath()
                      + "/member/"
                      + articleAuthorName
                      + "'>"
                      + articleAuthorName
                      + "</a>")
              .replace(
                  "{article}",
                  "<a target='_blank' rel='nofollow' href='"
                      + articlePermalink
                      + "'>"
                      + articleTitle
                      + "</a>");
      timeline.put(Common.CONTENT, content);

      timelineMgmtService.addTimeline(timeline);

      // 'Broadcast' Notification
      if (Article.ARTICLE_TYPE_C_CITY_BROADCAST == originalArticle.optInt(Article.ARTICLE_TYPE)) {
        final String city = originalArticle.optString(Article.ARTICLE_CITY);

        if (StringUtils.isNotBlank(city)) {
          final JSONObject requestJSONObject = new JSONObject();
          requestJSONObject.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, 1);
          requestJSONObject.put(Pagination.PAGINATION_PAGE_SIZE, Integer.MAX_VALUE);
          requestJSONObject.put(Pagination.PAGINATION_WINDOW_SIZE, Integer.MAX_VALUE);

          final long latestLoginTime = DateUtils.addDays(new Date(), -15).getTime();
          requestJSONObject.put(UserExt.USER_LATEST_LOGIN_TIME, latestLoginTime);
          requestJSONObject.put(UserExt.USER_CITY, city);

          final JSONObject result = userQueryService.getUsersByCity(requestJSONObject);
          final JSONArray users = result.optJSONArray(User.USERS);

          for (int i = 0; i < users.length(); i++) {
            final String userId = users.optJSONObject(i).optString(Keys.OBJECT_ID);

            if (userId.equals(articleAuthorId)) {
              continue;
            }

            final JSONObject notification = new JSONObject();
            notification.put(Notification.NOTIFICATION_USER_ID, userId);
            notification.put(Notification.NOTIFICATION_DATA_ID, articleId);

            notificationMgmtService.addBroadcastNotification(notification);
          }

          LOGGER.info("City broadcast [" + users.length() + "]");
        }
      }
    } catch (final Exception e) {
      LOGGER.log(Level.ERROR, "Sends the article notification failed", e);
    }
  }