/*
 * Copyright (C) 2003-2010 eXo Platform SAS.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU 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.exoplatform.faq.service.impl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import javax.jcr.ImportUUIDBehavior;
import javax.jcr.Item;
import javax.jcr.ItemExistsException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.Workspace;
import javax.jcr.observation.Event;
import javax.jcr.observation.ObservationManager;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;
import javax.jcr.query.Row;
import javax.jcr.query.RowIterator;

import org.apache.commons.lang.StringUtils;
import org.exoplatform.commons.utils.ActivityTypeUtils;
import org.exoplatform.container.component.ComponentPlugin;
import org.exoplatform.faq.service.Answer;
import org.exoplatform.faq.service.Cate;
import org.exoplatform.faq.service.Category;
import org.exoplatform.faq.service.CategoryInfo;
import org.exoplatform.faq.service.CategoryTree;
import org.exoplatform.faq.service.Comment;
import org.exoplatform.faq.service.DataStorage;
import org.exoplatform.faq.service.FAQEventQuery;
import org.exoplatform.faq.service.FAQNodeTypes;
import org.exoplatform.faq.service.FAQServiceUtils;
import org.exoplatform.faq.service.FAQSetting;
import org.exoplatform.faq.service.FileAttachment;
import org.exoplatform.faq.service.JCRPageList;
import org.exoplatform.faq.service.MessageBuilder;
import org.exoplatform.faq.service.ObjectSearchResult;
import org.exoplatform.faq.service.Question;
import org.exoplatform.faq.service.QuestionInfo;
import org.exoplatform.faq.service.QuestionLanguage;
import org.exoplatform.faq.service.QuestionNodeListener;
import org.exoplatform.faq.service.QuestionPageList;
import org.exoplatform.faq.service.SubCategoryInfo;
import org.exoplatform.faq.service.Utils;
import org.exoplatform.faq.service.Watch;
import org.exoplatform.faq.service.search.AnswerSearchResult;
import org.exoplatform.forum.common.CommonUtils;
import org.exoplatform.forum.common.EmailNotifyPlugin;
import org.exoplatform.forum.common.NotifyInfo;
import org.exoplatform.forum.common.UserHelper;
import org.exoplatform.forum.common.conf.RoleRulesPlugin;
import org.exoplatform.forum.common.jcr.KSDataLocation;
import org.exoplatform.forum.common.jcr.PropertyReader;
import org.exoplatform.forum.common.jcr.SessionManager;
import org.exoplatform.services.jcr.ext.common.SessionProvider;
import org.exoplatform.services.jcr.impl.core.query.QueryImpl;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.mail.Message;
import org.exoplatform.services.security.ConversationState;

import com.sun.syndication.feed.synd.SyndContent;
import com.sun.syndication.feed.synd.SyndContentImpl;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndEntryImpl;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.feed.synd.SyndFeedImpl;
import com.sun.syndication.io.SyndFeedOutput;

public class JCRDataStorage implements DataStorage, FAQNodeTypes {

  private static final Log        log                  = ExoLogger.getLogger(JCRDataStorage.class);

  protected Map<String, String>   serverConfig_        = new HashMap<String, String>();

  private Map<String, NotifyInfo> messagesInfoMap_     = new HashMap<String, NotifyInfo>();

  final Queue<NotifyInfo>         pendingMessagesQueue = new ConcurrentLinkedQueue<NotifyInfo>();

  private final String            ADMIN_               = "ADMIN".intern();

  private List<RoleRulesPlugin>   rulesPlugins_        = new ArrayList<RoleRulesPlugin>();

  final static Pattern    highlightPattern             = Pattern.compile(Utils.HIGHLIGHT_PATTERN);
  
  private SessionManager          sessionManager;

  private KSDataLocation          dataLocator;
  
  private final int EXCERPT_MAX_LENGTH = 430;

  public JCRDataStorage(KSDataLocation dataLocator) throws Exception {
    this.dataLocator = dataLocator;
    sessionManager = dataLocator.getSessionManager();
  }

  @Override
  public void addPlugin(ComponentPlugin plugin) throws Exception {
    try {
      serverConfig_ = ((EmailNotifyPlugin) plugin).getServerConfiguration();
    } catch (Exception e) {
      log.error("\nFailed to add plugin\n ", e);
    }
  }

  @Override
  public void addRolePlugin(ComponentPlugin plugin) throws Exception {
    try {
      if (plugin instanceof RoleRulesPlugin) {
        rulesPlugins_.add((RoleRulesPlugin) plugin);
      }
    } catch (Exception e) {
      log.error("Failed to add role plugin\n", e);
    }
  }

  @Override
  public boolean isAdminRole(String userName) throws Exception {
    String name = userName;
    if (name == null) {
      name = ConversationState.getCurrent().getIdentity().getUserId();
    }
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node cateHomeNode = getCategoryHome(sProvider, null);
      for (int i = 0; i < rulesPlugins_.size(); ++i) {
        List<String> list = new ArrayList<String>();
        list.addAll(rulesPlugins_.get(i).getRules(this.ADMIN_));
        list.addAll(new PropertyReader(cateHomeNode).list(EXO_MODERATORS, new ArrayList<String>()));
        if (list.contains(name))
          return true;
        if (Utils.hasPermission(list, UserHelper.getAllGroupAndMembershipOfUser(userName)))
          return true;
      }
    } catch (Exception e) {
      log.debug("Check user whether is admin: ", e);
    }
    return false;
  }

  @Override
  public List<String> getAllFAQAdmin() throws Exception {
    List<String> list = new ArrayList<String>();
    try {
      for (int i = 0; i < rulesPlugins_.size(); ++i) {
        list.addAll(rulesPlugins_.get(i).getRules(this.ADMIN_));
      }
      list = FAQServiceUtils.getUserPermission(list.toArray(new String[] {}));
    } catch (Exception e) {
      log.error("Failed to get all FAQ admin: ", e);
    }
    return list;
  }

  @Override
  public void getUserSetting(String userName, FAQSetting faqSetting) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node userSettingNode = getUserSettingHome(sProvider).getNode(userName);
      PropertyReader reader = new PropertyReader(userSettingNode);
      faqSetting.setOrderBy(reader.string(EXO_ORDE_BY, EMPTY_STR));
      faqSetting.setOrderType(reader.string(EXO_ORDE_TYPE, EMPTY_STR));
      faqSetting.setSortQuestionByVote(reader.bool(EXO_SORT_QUESTION_BY_VOTE));
    } catch (Exception e) {
      saveFAQSetting(faqSetting, userName);
    }
  }

  @Override
  public void saveFAQSetting(FAQSetting faqSetting, String userName) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node userSettingNode = getUserSettingHome(sProvider).getNode(userName);
      userSettingNode.setProperty(EXO_ORDE_BY, faqSetting.getOrderBy());
      userSettingNode.setProperty(EXO_ORDE_TYPE, faqSetting.getOrderType());
      userSettingNode.setProperty(EXO_SORT_QUESTION_BY_VOTE, faqSetting.isSortQuestionByVote());
      userSettingNode.save();
    } catch (PathNotFoundException e) {
      Node userSettingNode = getUserSettingHome(sProvider).addNode(userName, EXO_FAQ_USER_SETTING);
      userSettingNode.setProperty(EXO_ORDE_BY, faqSetting.getOrderBy());
      userSettingNode.setProperty(EXO_ORDE_TYPE, faqSetting.getOrderType());
      userSettingNode.setProperty(EXO_SORT_QUESTION_BY_VOTE, faqSetting.isSortQuestionByVote());
      userSettingNode.getSession().save();
    }
  }

  @Override
  public FileAttachment getUserAvatar(String userName) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node node = getKSUserAvatarHomeNode(sProvider).getNode(userName);
      return getFileAttachment(node);
    } catch (Exception e) {
      return null;
    }
  }
  
  @Override
  public void saveUserAvatar(String userId, FileAttachment fileAttachment) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node ksAvatarHomeNode = getKSUserAvatarHomeNode(sProvider);
      Node avatarNode;
      if (ksAvatarHomeNode.hasNode(userId))
        avatarNode = ksAvatarHomeNode.getNode(userId);
      else
        avatarNode = ksAvatarHomeNode.addNode(userId, NT_FILE);
      FAQServiceUtils.reparePermissions(avatarNode, "any");
      Node nodeContent;
      if (avatarNode.hasNode(JCR_CONTENT))
        nodeContent = avatarNode.getNode(JCR_CONTENT);
      else
        nodeContent = avatarNode.addNode(JCR_CONTENT, NT_RESOURCE);
      nodeContent.setProperty(JCR_MIME_TYPE, fileAttachment.getMimeType());
      nodeContent.setProperty(JCR_DATA, fileAttachment.getInputStream());
      nodeContent.setProperty(JCR_LAST_MODIFIED, CommonUtils.getGreenwichMeanTime().getTimeInMillis());
      if (avatarNode.isNew())
        ksAvatarHomeNode.getSession().save();
      else
        ksAvatarHomeNode.save();
    } catch (Exception e) {
      log.error("Failed to save user avatar: ", e);
    }
  }

  @Override
  public void setDefaultAvatar(String userName) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node avatarHome = getKSUserAvatarHomeNode(sProvider);
      if (avatarHome.hasNode(userName)) {
        Node node = avatarHome.getNode(userName);
        if (node.isNodeType(NT_FILE)) {
          node.remove();
          avatarHome.save();
        }
      }
    } catch (Exception e) {
      log.error("Failed to set default avatar: ", e);
    }
  }

  public NodeIterator getQuestionsIterator(SessionProvider sProvider) throws Exception {
    try {
      Node faqHome = getFAQServiceHome(sProvider);
      return getQuestionsIterator(faqHome, EMPTY_STR, true);
    } catch (Exception e) {
      log.error("Failed to get question iterator: ", e);
      return null;
    }
  }

  private NodeIterator getQuestionsIterator(Node parentNode, String strQuery, boolean isAll) throws Exception {
    StringBuffer queryString = new StringBuffer(JCR_ROOT).append(parentNode.getPath()).append((isAll) ? "//" : "/").append("element(*,exo:faqQuestion)").append(strQuery);
    QueryManager qm = parentNode.getSession().getWorkspace().getQueryManager();
    Query query = qm.createQuery(queryString.toString(), Query.XPATH);
    QueryResult result = query.execute();
    return result.getNodes();
  }

  public void initQuestionNodeListeners() throws Exception {
    SessionProvider sProvider = SessionProvider.createSystemProvider();
    try {
      ObservationManager observation = sessionManager.getSession(sProvider).getWorkspace().getObservationManager();
      QuestionNodeListener listener = new QuestionNodeListener();
      String[] properties = new String[] {EXO_ANSWER, EXO_FAQ_QUESTION, EXO_FAQ_LANGUAGE};
      observation.addEventListener(listener, Event.NODE_ADDED | Event.NODE_REMOVED | Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED, "/", true, null, properties, false);
    } catch (Exception e) {
      log.error("Failed to get question iterator: ", e);
    } finally {
      sProvider.close();
    }
  }

  @Override
  public boolean initRootCategory() throws Exception {
    SessionProvider sProvider = SessionProvider.createSystemProvider();
    try {
      Node faqServiceHome = getFAQServiceHome(sProvider);
      if (faqServiceHome.hasNode(Utils.CATEGORY_HOME)) {
        log.info("root category is already created");
        return false;
      }
      Node categoryHome = faqServiceHome.addNode(Utils.CATEGORY_HOME, EXO_FAQ_CATEGORY);
      categoryHome.addMixin(MIX_FAQ_SUB_CATEGORY);
      categoryHome.setProperty(EXO_NAME, "Answers");
      categoryHome.setProperty(EXO_IS_VIEW, true);
      faqServiceHome.save();
      log.info("Initialized root category : " + categoryHome.getPath());
      return true;
    } catch (Exception e) {
      log.error("Could not initialize root category", e);
      return false;
    } finally {
      sProvider.close();
    }
  }

  @Override
  public byte[] getTemplate() throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node templateHome = getTemplateHome(sProvider);
      Node fileNode = templateHome.getNode(Utils.UI_FAQ_VIEWER);
      if (fileNode.isNodeType(NT_FILE)) {
        Node contentNode = fileNode.getNode(JCR_CONTENT);
        InputStream inputStream = contentNode.getProperty(JCR_DATA).getStream();
        byte[] data = new byte[inputStream.available()];
        inputStream.read(data);
        inputStream.close();
        return data;
      }
    } catch (Exception e) {
      log.error("Failed to get template", e);
    }
    return null;
  }

  @Override
  public void saveTemplate(String str) throws Exception {
    SessionProvider sProvider = SessionProvider.createSystemProvider();
    try {
      Node templateHome = getTemplateHome(sProvider);
      Node fileNode;
      try {
        fileNode = templateHome.getNode(Utils.UI_FAQ_VIEWER);
      } catch (Exception e) {
        fileNode = templateHome.addNode(Utils.UI_FAQ_VIEWER, NT_FILE);
      }
      Node nodeContent = null;
      InputStream inputStream = null;
      byte[] byte_ = str.getBytes();
      inputStream = new ByteArrayInputStream(byte_);
      try {
        nodeContent = fileNode.addNode(JCR_CONTENT, NT_RESOURCE);
      } catch (Exception e) {
        nodeContent = fileNode.getNode(JCR_CONTENT);
      }
      nodeContent.setProperty(JCR_MIME_TYPE, "application/x-groovy+html");
      nodeContent.setProperty(JCR_DATA, inputStream);
      nodeContent.setProperty(JCR_LAST_MODIFIED, CommonUtils.getGreenwichMeanTime().getTimeInMillis());
      if (templateHome.isNew()) {
        templateHome.getSession().save();
      } else {
        templateHome.save();
      }
    } catch (Exception e) {
      log.error("Failed to save template: ", e);
    } finally {
      sProvider.close();
    }
  }

  protected Value[] booleanToValues(Node node, Boolean[] bools) throws Exception {
    if (bools == null)
      return new Value[] { node.getSession().getValueFactory().createValue(true) };
    Value[] values = new Value[bools.length];
    for (int i = 0; i < values.length; i++) {
      values[i] = node.getSession().getValueFactory().createValue(bools[i]);
    }
    return values;
  }

  private static boolean questionHasAnswer(Node questionNode) throws Exception {
    if (questionNode.hasNode(Utils.ANSWER_HOME) && questionNode.getNode(Utils.ANSWER_HOME).hasNodes())
      return true;
    return false;
  }

  private void sendNotifyWatcher(SessionProvider sProvider, Question question, FAQSetting faqSetting, boolean isNew) {
    // Send notification when add new question in watching category
    List<String> emails = new ArrayList<String>();
    List<String> users = new ArrayList<String>();
    List<String> emailsList = new ArrayList<String>();
    emailsList.add(question.getEmail());
    try {
      Node cate = getCategoryNode(sProvider, question.getCategoryPath());
      PropertyReader reader = new PropertyReader(cate);
      // watch in category parent
      emails.addAll(reader.list(EXO_EMAIL_WATCHING, new ArrayList<String>()));
      users.addAll(reader.list(EXO_USER_WATCHING, new ArrayList<String>()));
      // watch in this question
      if (!CommonUtils.isEmpty(question.getEmailsWatch())) {
        emails.addAll(Arrays.asList(question.getEmailsWatch()));
        users.addAll(Arrays.asList(question.getUsersWatch()));
      }
      if (!question.isActivated() || (!question.isApproved())) {
        // only send notification to administrations or moderators
        List<String> moderators = reader.list(EXO_MODERATORS, new ArrayList<String>());
        List<String> temps = new ArrayList<String>();
        int i = 0;
        for (String user : users) {
          if (!temps.contains(user)) {
            temps.add(user);
            if (isAdminRole(user) || Utils.hasPermission(moderators, UserHelper.getAllGroupAndMembershipOfUser(user))) {
              emailsList.add(emails.get(i));
            }
          }
          ++i;
        }
      } else {
        emailsList.addAll(emails);
      }
      if (!emailsList.isEmpty()) {
        MessageBuilder messageBuilder = new MessageBuilder();
        messageBuilder.setType(MessageBuilder.TYPESEND.NEW_QUESTION);
        messageBuilder.setCategoryName(reader.string(EXO_NAME, "Root"));
        messageBuilder.setContent(faqSetting.getEmailSettingContent());
        messageBuilder.setSubject(faqSetting.getEmailSettingSubject());
        messageBuilder.setQuestionOwner(question.getAuthor());
        messageBuilder.setQuestionEmail(question.getEmail());
        messageBuilder.setQuestionLink(question.getLink());
        messageBuilder.setQuestionDetail(question.getDetail());
        messageBuilder.setQuestionContent(question.getQuestion());
        
        if (question.getAnswers() != null && question.getAnswers().length > 0) {
          messageBuilder.setQuestionResponse(question.getAnswers()[0].getResponses());
        } else {
          messageBuilder.setQuestionResponse(EMPTY_STR);
        }
        sendEmailNotification(emailsList, messageBuilder.getMessage());
      }
    } catch (Exception e) {
      log.error("Failed to send a nofify for category watcher: ", e);
    }
  }

  @Override
  public List<QuestionLanguage> getQuestionLanguages(String questionId){
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    List<QuestionLanguage> listQuestionLanguage = new ArrayList<QuestionLanguage>();
    try {
      Node questionNode = getFAQServiceHome(sProvider).getNode(questionId);
      try {
        listQuestionLanguage.add(getQuestionLanguage(questionNode));
      } catch (Exception e) {
        log.debug("Adding a question node failed: ", e);
      }
      if (questionNode.hasNode(Utils.LANGUAGE_HOME)) {
        Node languageNode = questionNode.getNode(Utils.LANGUAGE_HOME);
        NodeIterator nodeIterator = languageNode.getNodes();
        while (nodeIterator.hasNext()) {
          try {
            listQuestionLanguage.add(getQuestionLanguage(nodeIterator.nextNode()));
          } catch (RepositoryException e) {
            log.debug(String.format("Failed to get languages for question %s", questionId), e);
          }
        }
      }
    } catch (Exception e) {
      log.error("Failed to get question language: ", e);
    }
    return listQuestionLanguage;
  }

  private QuestionLanguage getQuestionLanguage(Node questionNode) throws RepositoryException{
    QuestionLanguage questionLanguage = new QuestionLanguage();
    questionLanguage.setState(QuestionLanguage.VIEW);
    questionLanguage.setId(questionNode.getName());
    PropertyReader reader = new PropertyReader(questionNode);
    questionLanguage.setLanguage(reader.string(EXO_LANGUAGE, EMPTY_STR));
    questionLanguage.setQuestion(reader.string(EXO_TITLE, EMPTY_STR));
    questionLanguage.setDetail(reader.string(EXO_NAME, EMPTY_STR));
    Comment[] comments = getComment(questionNode);
    Answer[] answers = getAnswers(questionNode);
    questionLanguage.setComments(comments);
    questionLanguage.setAnswers(answers);
    return questionLanguage;
  }

  @Override
  public void deleteAnswer(String questionId, String answerId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node questionNode = getFAQServiceHome(sProvider).getNode(questionId);
      Node answerNode = questionNode.getNode(Utils.ANSWER_HOME).getNode(answerId);
      answerNode.remove();
      questionNode.save();
    } catch (Exception e) {
      log.error("Failed to delete a answer: ", e);
    }
  }

  @Override
  public void deleteComment(String questionId, String commentId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node questionNode = getFAQServiceHome(sProvider).getNode(questionId);
      Node commnetNode = questionNode.getNode(Utils.COMMENT_HOME).getNode(commentId);
      commnetNode.remove();
      questionNode.save();
    } catch (Exception e) {
      log.error("Failed to delete a commnent: ", e);
    }
  }

  private Answer[] getAnswers(Node questionNode) {
    try {
      if (!questionNode.hasNode(Utils.ANSWER_HOME))
        return new Answer[] {};
      NodeIterator nodeIterator = questionNode.getNode(Utils.ANSWER_HOME).getNodes();
      List<Answer> answers = new ArrayList<Answer>();
      Answer ans;
      String language = questionNode.getProperty(EXO_LANGUAGE).getString();
      while (nodeIterator.hasNext()) {
        try {
          Node node = nodeIterator.nextNode();
          ans = getAnswerByNode(node);
          ans.setLanguage(language);
          answers.add(ans);
        } catch (Exception e) {
          log.error("Failed to get anwser", e);
        }
      }
      return answers.toArray(new Answer[] {});
    } catch (Exception e) {
      log.error("Failed to get answer: ", e);
    }
    return new Answer[] {};
  }

  private Answer getAnswerByNode(Node answerNode) throws Exception {
    Answer answer = new Answer();
    answer.setId(answerNode.getName());
    PropertyReader reader = new PropertyReader(answerNode);
    answer.setResponses(reader.string(EXO_RESPONSES, EMPTY_STR));
    answer.setResponseBy(reader.string(EXO_RESPONSE_BY, EMPTY_STR));
    answer.setFullName(reader.string(EXO_FULL_NAME, EMPTY_STR));
    answer.setDateResponse((answerNode.getProperty(EXO_DATE_RESPONSE).getDate().getTime()));
    answer.setUsersVoteAnswer(reader.strings(EXO_USERS_VOTE_ANSWER, new String[] {}));
    answer.setMarkVotes(reader.l(EXO_MARK_VOTES));
    answer.setApprovedAnswers(reader.bool(EXO_APPROVE_RESPONSES, true));
    answer.setActivateAnswers(reader.bool(EXO_ACTIVATE_RESPONSES, true));
    answer.setPostId(reader.string(EXO_POST_ID, EMPTY_STR));
    String path = answerNode.getPath();
    answer.setPath(path.substring(path.indexOf(Utils.FAQ_APP) + Utils.FAQ_APP.length() + 1));
    return answer;
  }

  @Override
  public JCRPageList getPageListAnswer(String questionId, boolean isSortByVote) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node questionNode = getFAQServiceHome(sProvider).getNode(questionId);
      if (questionNode.hasNode(Utils.ANSWER_HOME)) {
        Node answerHome = questionNode.getNode(Utils.ANSWER_HOME);
        QueryManager qm = answerHome.getSession().getWorkspace().getQueryManager();
        StringBuffer queryString = new StringBuffer(JCR_ROOT).append(answerHome.getPath()).append("//element(*,exo:answer)");
        if ((Boolean) isSortByVote == null)
          queryString.append("order by @exo:dateResponse ascending");
        else if (isSortByVote)
          queryString.append("order by @exo:MarkVotes ascending");
        else
          queryString.append("order by @exo:MarkVotes descending");
        Query query = qm.createQuery(queryString.toString(), Query.XPATH);
        QueryResult result = query.execute();
        QuestionPageList pageList = new QuestionPageList(result.getNodes(), 10, queryString.toString(), true);
        return pageList;
      }
    } catch (Exception e) {
      log.error("Failed to get page list answers", e);
    }
    return null;
  }

  @Override
  public void saveAnswer(String questionId, Answer answer, boolean isNew) throws Exception {
    Answer[] answers = { answer };
    saveAnswer(questionId, answers);
  }

  @Override
  public void saveAnswer(String questionId, Answer[] answers) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node quesNode = getFAQServiceHome(sProvider).getNode(questionId);
      if (!quesNode.isNodeType(MIX_FAQI_1_8N)) {
        quesNode.addMixin(MIX_FAQI_1_8N);
      }
      Node answerHome;
      String qId = quesNode.getName();
      String categoryId = quesNode.getProperty(EXO_CATEGORY_ID).getString();
      String defaultLang = quesNode.getProperty(EXO_LANGUAGE).getString();

      for (Answer answer : answers) {

        if (answer.getLanguage().equals(defaultLang)) {
          try {
            answerHome = quesNode.getNode(Utils.ANSWER_HOME);
          } catch (Exception e) {
            answerHome = quesNode.addNode(Utils.ANSWER_HOME, EXO_ANSWER_HOME);
          }
        } else { // answer for other languages
          Node langNode = getLanguageNodeByLanguage(quesNode, answer.getLanguage());
          try {
            answerHome = langNode.getNode(Utils.ANSWER_HOME);
          } catch (Exception e) {
            answerHome = langNode.addNode(Utils.ANSWER_HOME, EXO_ANSWER_HOME);
          }
        }
        saveAnswer(answer, answerHome, qId, categoryId);
      }
      quesNode.save();
    } catch (Exception e) {
      log.error("Failed to save answer: ", e);
    }
  }

  private void saveAnswer(Answer answer, Node answerHome, String questionId, String categoryId) throws Exception {
    Node answerNode;
    try {
      answerNode = answerHome.getNode(answer.getId());
    } catch (PathNotFoundException e) {
      answerNode = answerHome.addNode(answer.getId(), EXO_ANSWER);
    }
    if (!answer.isNew()) { // remove answer
      answerNode.remove();
      return;
    }
    try {
      if (answerNode.isNew()) {
        answerNode.setProperty(EXO_DATE_RESPONSE, CommonUtils.getGreenwichMeanTime());
        answerNode.setProperty(EXO_ID, answer.getId());
        answerNode.setProperty(EXO_APPROVE_RESPONSES, answer.getApprovedAnswers());
        answerNode.setProperty(EXO_ACTIVATE_RESPONSES, answer.getActivateAnswers());
      } else {
        if (new PropertyReader(answerNode).bool(EXO_APPROVE_RESPONSES, false) != answer.getApprovedAnswers())
          answerNode.setProperty(EXO_APPROVE_RESPONSES, answer.getApprovedAnswers());
        if (new PropertyReader(answerNode).bool(EXO_ACTIVATE_RESPONSES, false) != answer.getActivateAnswers())
          answerNode.setProperty(EXO_ACTIVATE_RESPONSES, answer.getActivateAnswers());
      }
      if (answer.getPostId() != null && answer.getPostId().length() > 0) {
        answerNode.setProperty(EXO_POST_ID, answer.getPostId());
      }
      answerNode.setProperty(EXO_RESPONSES, answer.getResponses());
      answerNode.setProperty(EXO_RESPONSE_BY, answer.getResponseBy());
      answerNode.setProperty(EXO_FULL_NAME, answer.getFullName());
      answerNode.setProperty(EXO_USERS_VOTE_ANSWER, answer.getUsersVoteAnswer());
      answerNode.setProperty(EXO_MARK_VOTES, answer.getMarkVotes());
      answerNode.setProperty(EXO_RESPONSE_LANGUAGE, answer.getLanguage());
      answerNode.setProperty(EXO_QUESTION_ID, questionId);
      answerNode.setProperty(EXO_CATEGORY_ID, categoryId);
    } catch (Exception e) {
      log.error("Failed to save Answer: ", e);
    }
  }

  @Override
  public void saveComment(String questionId, Comment comment, boolean isNew) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node quesNode = getFAQServiceHome(sProvider).getNode(questionId);
      if (!quesNode.isNodeType(MIX_FAQI_1_8N)) {
        quesNode.addMixin(MIX_FAQI_1_8N);
      }
      Node commentHome = null;
      try {
        commentHome = quesNode.getNode(Utils.COMMENT_HOME);
      } catch (PathNotFoundException e) {
        commentHome = quesNode.addNode(Utils.COMMENT_HOME, EXO_COMMENT_HOME);
      }
      Node commentNode;
      if (isNew) {
        commentNode = commentHome.addNode(comment.getId(), EXO_COMMENT);
        commentNode.setProperty(EXO_DATE_COMMENT, CommonUtils.getGreenwichMeanTime());
        commentNode.setProperty(EXO_ID, comment.getId());
      } else {
        commentNode = commentHome.getNode(comment.getId());
      }

      if (comment.getPostId() != null && comment.getPostId().length() > 0) {
        commentNode.setProperty(EXO_POST_ID, comment.getPostId());
      }
      commentNode.setProperty(EXO_COMMENTS, comment.getComments());
      commentNode.setProperty(EXO_COMMENT_BY, comment.getCommentBy());
      commentNode.setProperty(EXO_FULL_NAME, comment.getFullName());
      commentNode.setProperty(EXO_CATEGORY_ID, quesNode.getProperty(EXO_CATEGORY_ID).getString());
      commentNode.setProperty(EXO_QUESTION_ID, quesNode.getName());
      commentNode.setProperty(EXO_COMMENT_LANGUAGE, quesNode.getProperty(EXO_LANGUAGE).getString());
      if (commentNode.isNew())
        quesNode.getSession().save();
      else
        quesNode.save();
    } catch (Exception e) {
      log.error("Failed to save comment: ", e);
    }
  }

  @Override
  public void saveAnswerQuestionLang(String questionId, Answer answer, String language, boolean isNew) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node quesNode = getFAQServiceHome(sProvider).getNode(questionId);
      Node answerHome = null;
      try {
        answerHome = quesNode.getNode(Utils.ANSWER_HOME);
      } catch (PathNotFoundException e) {
        answerHome = quesNode.addNode(Utils.ANSWER_HOME, EXO_ANSWER_HOME);
      }
      Node answerNode;
      if (isNew) {
        answerNode = answerHome.addNode(answer.getId(), EXO_ANSWER);
        answerNode.setProperty("exo:dateResponses", CommonUtils.getGreenwichMeanTime());
        answerNode.setProperty(EXO_APPROVE_RESPONSES, answer.getApprovedAnswers());
        answerNode.setProperty(EXO_ACTIVATE_RESPONSES, answer.getActivateAnswers());
      } else {
        answerNode = answerHome.getNode(answer.getId());
        if (new PropertyReader(answerNode).bool(EXO_APPROVE_RESPONSES, false) != answer.getApprovedAnswers())
          answerNode.setProperty(EXO_APPROVE_RESPONSES, answer.getApprovedAnswers());
        if (new PropertyReader(answerNode).bool(EXO_ACTIVATE_RESPONSES, false) != answer.getActivateAnswers())
          answerNode.setProperty(EXO_ACTIVATE_RESPONSES, answer.getActivateAnswers());
      }
      answerNode.setProperty(EXO_RESPONSES, answer.getResponses());
      answerNode.setProperty(EXO_RESPONSE_BY, answer.getResponseBy());
      answerNode.setProperty(EXO_FULL_NAME, answer.getFullName());
      answerNode.setProperty(EXO_USERS_VOTE_ANSWER, answer.getUsersVoteAnswer());
    } catch (Exception e) {
      log.error("Failed to save answer question language: ", e);
    }
  }

  @Override
  public Answer getAnswerById(String questionId, String answerid) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node answerNode = getFAQServiceHome(sProvider).getNode(questionId).getNode(Utils.ANSWER_HOME).getNode(answerid);
      return getAnswerByNode(answerNode);
    } catch (Exception e) {
      log.debug("Failed to get answer by id.", e);
    }
    return null;
  }

  private Comment[] getComment(Node questionNode) {
    try {
      if (questionNode == null || !questionNode.hasNode(Utils.COMMENT_HOME))
        return new Comment[] {};
      NodeIterator nodeIterator = questionNode.getNode(Utils.COMMENT_HOME).getNodes();
      Comment[] comments = new Comment[(int) nodeIterator.getSize()];
      Node commentNode = null;
      int i = 0;
      while (nodeIterator.hasNext()) {
        commentNode = nodeIterator.nextNode();
        comments[i] = getCommentByNode(commentNode);
        i++;
      }
      return comments;
    } catch (Exception e) {
      log.error("Failed to get comment: ", e);
      return new Comment[] {};
    }
  }

  private static int getCommentSize(Node questionNode) {
    try {
      if (questionNode == null || !questionNode.hasNode(Utils.COMMENT_HOME))
        return 0;
      NodeIterator nodeIterator = questionNode.getNode(Utils.COMMENT_HOME).getNodes();
      return (int) nodeIterator.getSize();
    } catch (Exception e) {
      return 0;
    }
  }

  @Override
  public JCRPageList getPageListComment(String questionId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node commentHome = getFAQServiceHome(sProvider).getNode(questionId + "/" + Utils.COMMENT_HOME);
      QueryManager qm = commentHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(commentHome.getPath()).append("//element(*,exo:comment)").append("order by @exo:dateComment ascending");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      QuestionPageList pageList = new QuestionPageList(result.getNodes(), 10, queryString.toString(), true);
      return pageList;
    } catch (Exception e) {
      log.debug("Failed to get page list comments", e);
      return null;
    }
  }

  private Comment getCommentByNode(Node commentNode) throws Exception {
    Comment comment = new Comment();
    comment.setId(commentNode.getName());
    PropertyReader reader = new PropertyReader(commentNode);
    comment.setComments((reader.string(EXO_COMMENTS, EMPTY_STR)));
    comment.setCommentBy((reader.string(EXO_COMMENT_BY, EMPTY_STR)));
    comment.setDateComment((commentNode.getProperty(EXO_DATE_COMMENT).getDate().getTime()));
    comment.setFullName((reader.string(EXO_FULL_NAME, EMPTY_STR)));
    comment.setPostId(reader.string(EXO_POST_ID, EMPTY_STR));
    return comment;
  }

  @Override
  public Comment getCommentById(String questionId, String commentId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node commentNode = getFAQServiceHome(sProvider).getNode(questionId + "/" + Utils.COMMENT_HOME + "/" + commentId);
      return getCommentByNode(commentNode);
    } catch (Exception e) {
      log.error("Failed to get comment by id: " + commentId, e);
      return null;
    }
  }

  private Node getLanguageNodeByLanguage(Node questionNode, String languge) throws Exception {
    NodeIterator nodeIterator = questionNode.getNode(Utils.LANGUAGE_HOME).getNodes();
    Node languageNode = null;
    while (nodeIterator.hasNext()) {
      languageNode = nodeIterator.nextNode();
      if (languageNode.getProperty(EXO_LANGUAGE).getString().equals(languge))
        return languageNode;
    }
    return null;
  }

  private void saveQuestion(Node questionNode, Question question, boolean isNew, SessionProvider sProvider, FAQSetting faqSetting) throws Exception {
    questionNode.setProperty(EXO_ID, questionNode.getName());
    questionNode.setProperty(EXO_NAME, question.getDetail());
    questionNode.setProperty(EXO_AUTHOR, question.getAuthor());
    questionNode.setProperty(EXO_EMAIL, question.getEmail());
    questionNode.setProperty(EXO_TITLE, question.getQuestion());
    Calendar calendar = CommonUtils.getGreenwichMeanTime();
    questionNode.setProperty(EXO_LAST_ACTIVITY, getLastActivityInfo(question.getAuthor(), calendar.getTimeInMillis()));
    if (isNew) {
      questionNode.setProperty(EXO_CREATED_DATE, calendar);
      questionNode.setProperty(EXO_LANGUAGE, question.getLanguage());
    }
    String cateId = questionNode.getParent().getParent().getName();
    questionNode.setProperty(EXO_CATEGORY_ID, cateId);
    questionNode.setProperty(EXO_IS_ACTIVATED, question.isActivated());
    questionNode.setProperty(EXO_IS_APPROVED, question.isApproved());
    questionNode.setProperty(EXO_USERS_VOTE, question.getUsersVote());
    questionNode.setProperty(EXO_MARK_VOTE, question.getMarkVote());
    questionNode.setProperty(EXO_LINK, CommonUtils.getURI(question.getLink()));
    List<FileAttachment> listFileAtt = question.getAttachMent();

    List<String> listNodeNames = new ArrayList<String>();
    if (!listFileAtt.isEmpty()) {
      for (FileAttachment att : listFileAtt) {
        listNodeNames.add(att.getNodeName());
        try {
          Node nodeFile = null;
          if (questionNode.hasNode(att.getNodeName()))
            nodeFile = questionNode.getNode(att.getNodeName());
          else
            nodeFile = questionNode.addNode(att.getNodeName(), EXO_FAQ_ATTACHMENT);
          // fix permission to download file in ie 6:
          FAQServiceUtils.reparePermissions(nodeFile, "any");
          Node nodeContent = null;
          if (nodeFile.hasNode(JCR_CONTENT))
            nodeContent = nodeFile.getNode(JCR_CONTENT);
          else
            nodeContent = nodeFile.addNode(JCR_CONTENT, EXO_FAQ_RESOURCE);
          nodeContent.setProperty(EXO_FILE_NAME, att.getName());
          nodeContent.setProperty(EXO_CATEGORY_ID, cateId);
          nodeContent.setProperty(JCR_MIME_TYPE, att.getMimeType());
          nodeContent.setProperty(JCR_DATA, att.getInputStream());
          nodeContent.setProperty(JCR_LAST_MODIFIED, CommonUtils.getGreenwichMeanTime().getTimeInMillis());
        } catch (Exception e) {
          log.error("Failed to save question: ", e);
        }
      }
    }
    // remove attachments
    NodeIterator nodeIterator = questionNode.getNodes();
    Node node = null;
    while (nodeIterator.hasNext()) {
      node = nodeIterator.nextNode();
      if (node.isNodeType(EXO_FAQ_ATTACHMENT) && !listNodeNames.contains(node.getName()))
        node.remove();
    }

    question.setId(questionNode.getName());
    sendNotifyWatcher(sProvider, question, faqSetting, isNew);
  }

  @Override
  public Node saveQuestion(Question question, boolean isAddNew, FAQSetting faqSetting) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node questionNode;
      Node questionHome;
      Node category;
      if (isAddNew) {
        category = getFAQServiceHome(sProvider).getNode(question.getCategoryPath());
        try {
          questionHome = category.getNode(Utils.QUESTION_HOME);
        } catch (PathNotFoundException ex) {
          questionHome = category.addNode(Utils.QUESTION_HOME, EXO_FAQ_QUESTION_HOME);
        }
        questionNode = questionHome.addNode(question.getId(), EXO_FAQ_QUESTION);
      } else {
        questionNode = getFAQServiceHome(sProvider).getNode(question.getPath());
      }
      saveQuestion(questionNode, question, isAddNew, sProvider, faqSetting);
      if (questionNode.isNew()) {
        questionNode.getSession().save();
      } else
        questionNode.save();

      return questionNode;
    } catch (Exception e) {
      log.error("Failed to save question ", e);
    }
    return null;
  }

  @Override
  public void removeQuestion(String questionId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node questionNode = getFAQServiceHome(sProvider).getNode(questionId);
      Node questionHome = questionNode.getParent();
      questionNode.remove();
      questionHome.save();
    } catch (Exception e) {
      log.error("Fail ro remove question: ", e);
    }
  }

  @Override
  public Comment getCommentById(Node questionNode, String commentId) throws Exception {
    try {
      Comment comment = new Comment();
      Node commentNode = questionNode.getNode(Utils.COMMENT_HOME).getNode(commentId);
      comment.setId(commentNode.getName());
      PropertyReader reader = new PropertyReader(commentNode);
      comment.setComments((reader.string(EXO_COMMENTS, EMPTY_STR)));
      comment.setCommentBy((reader.string(EXO_COMMENT_BY, EMPTY_STR)));
      comment.setDateComment(reader.date(EXO_DATE_COMMENT));
      comment.setPostId(reader.string(EXO_POST_ID, EMPTY_STR));
      return comment;
    } catch (Exception e) {
      log.error("Failed to get comment through id: ", e);
      return null;
    }
  }

  private Question getQuestion(Node questionNode) throws Exception {
    if(questionNode == null) return null;
    Question question = new Question();
    PropertyReader reader = new PropertyReader(questionNode);
    question.setId(questionNode.getName());
    question.setLanguage(reader.string(EXO_LANGUAGE, EMPTY_STR));
    question.setDetail(reader.string(EXO_NAME, EMPTY_STR));
    question.setAuthor(reader.string(EXO_AUTHOR, EMPTY_STR));
    question.setEmail(reader.string(EXO_EMAIL, EMPTY_STR));
    question.setQuestion(reader.string(EXO_TITLE, EMPTY_STR));
    question.setCreatedDate(reader.date(EXO_CREATED_DATE));
    question.setActivated(reader.bool(EXO_IS_ACTIVATED, true));
    question.setApproved(reader.bool(EXO_IS_APPROVED, true));
    question.setRelations(reader.strings(EXO_RELATIVES, new String[] {}));
    question.setNameAttachs(reader.strings(EXO_NAME_ATTACHS, new String[] {}));
    question.setUsersVote(reader.strings(EXO_USERS_VOTE, new String[] {}));
    question.setMarkVote(reader.d(EXO_MARK_VOTE));
    question.setEmailsWatch(reader.strings(EXO_EMAIL_WATCHING, new String[] {}));
    question.setUsersWatch(reader.strings(EXO_USER_WATCHING, new String[] {}));
    question.setTopicIdDiscuss(reader.string(EXO_TOPIC_ID_DISCUSS, EMPTY_STR));
    question.setLink(reader.string(EXO_LINK, EMPTY_STR));
    question.setLastActivity(reader.string(EXO_LAST_ACTIVITY, EMPTY_STR));
    question.setNumberOfPublicAnswers(reader.l(EXO_NUMBER_OF_PUBLIC_ANSWERS, 0));

    String path = questionNode.getPath();
    question.setPath(path.substring(path.indexOf(Utils.FAQ_APP) + Utils.FAQ_APP.length() + 1));
    question.setCategoryId(reader.string(EXO_CATEGORY_ID, EMPTY_STR));
    Node node = questionNode.getParent();
    while (!node.isNodeType(EXO_FAQ_CATEGORY)) {
      node = node.getParent();
      question.setCategoryId(node.getName());
      question.setCategoryPath(node.getPath());
    }
    question.setAttachMent(getFileAttachments(questionNode));
    question.setAnswers(getAnswers(questionNode));
    question.setComments(getComment(questionNode));
    return question;
  }
  
  
  private static FileAttachment getFileAttachment(Node node) throws Exception {
    FileAttachment attachment = null;
    try {
      if (node.isNodeType(EXO_FAQ_ATTACHMENT) || node.isNodeType(NT_FILE)) {
        PropertyReader readerContent;
        String workspace = node.getSession().getWorkspace().getName();
        attachment = new FileAttachment();
        readerContent = new PropertyReader(node.getNode(JCR_CONTENT));
        attachment.setId(node.getPath());
        attachment.setMimeType(readerContent.string(JCR_MIME_TYPE));
        attachment.setNodeName(node.getName());
        attachment.setWorkspace(workspace);
        attachment.setPath(CommonUtils.SLASH + workspace + node.getPath());
        attachment.setSize(readerContent.stream(JCR_DATA).available());
        String fileName = readerContent.string(EXO_FILE_NAME);
        if (CommonUtils.isEmpty(fileName)) {
          String type = attachment.getMimeType(); 
          if (type.indexOf(CommonUtils.SLASH) > 0) {
            type = type.substring(type.indexOf(CommonUtils.SLASH) + 1);
          }
          fileName = "avatar." + type;
        }
        attachment.setName(fileName);
      }
    } catch (Exception e) {
      logDebug("Failed to get attachment in node: " + node.getName(), e);
    }
    return attachment;
  }
  
  public static List<FileAttachment> getFileAttachments(Node node) throws Exception {
    List<FileAttachment> listFile = new ArrayList<FileAttachment>();
    NodeIterator iter = node.getNodes();
    while (iter.hasNext()) {
      FileAttachment attachment = getFileAttachment(iter.nextNode());
      if (attachment != null) {
        listFile.add(attachment);
      }
    }
    return listFile;
  }

  @Override
  public Question getQuestionById(String questionId) throws Exception {
    SessionProvider sessionProvider = CommonUtils.createSystemProvider();
    Node questionNode = getQuestionNode(sessionProvider, questionId);
    return getQuestion(questionNode);
  }

  private List<String> getViewableCategoryIds(SessionProvider sessionProvider) throws Exception {
    List<String> listId = new ArrayList<String>();
    Node cateHomeNode = getCategoryHome(sessionProvider, null);
    StringBuffer queryString = new StringBuffer(JCR_ROOT).append(cateHomeNode.getPath()).append("//element(*,exo:faqCategory)[@exo:isView='true'] order by @exo:createdDate descending");
    QueryManager qm = cateHomeNode.getSession().getWorkspace().getQueryManager();
    Query query = qm.createQuery(queryString.toString(), Query.XPATH);
    QueryResult result = query.execute();
    NodeIterator iter = result.getNodes();
    while (iter.hasNext()) {
      listId.add(iter.nextNode().getName());
    }
    listId.add(Utils.CATEGORY_HOME);
    return listId;
  }

  private List<String> getRetrictedCategories(String userId, List<String> usermemberships) throws Exception {
    List<String> categoryList = new ArrayList<String>();
    SessionProvider sessionProvider = CommonUtils.createSystemProvider();
    try {
      Node faqHome = getFAQServiceHome(sessionProvider);
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(faqHome.getPath()).append("//element(*,exo:faqCategory)[@exo:userPrivate != ''] order by @exo:createdDate descending");
      QueryManager qm = faqHome.getSession().getWorkspace().getQueryManager();
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator iter = result.getNodes();
      boolean isAudience = false;
      List<String> audiences;
      while (iter.hasNext()) {
        if (usermemberships.size() > 0) {
          Node cat = iter.nextNode();
          try {
            audiences = new PropertyReader(cat).list(EXO_USER_PRIVATE, new ArrayList<String>());
            isAudience = false;
            for (String id : usermemberships) {
              for (String audien : audiences) {
                if (id.equals(audien)) {
                  isAudience = true;
                  break;
                }
              }
              if (isAudience)
                break;
            }
            if (!isAudience)
              categoryList.add(cat.getName());
          } catch (Exception e) {
            log.error("Failed to check audience ", e);
          }
        } else {
          categoryList.add(iter.nextNode().getName());
        }
      }
    } catch (Exception e) {
      log.error("Failed to get restricte category: ", e);
    }
    return categoryList;
  }

  @Override
  public QuestionPageList getAllQuestions() throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node categoryHome = getCategoryHome(sProvider, null);
      QueryManager qm = categoryHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(categoryHome.getPath()).append("//element(*,exo:faqQuestion)[");
      List<String> listIds = getViewableCategoryIds(sProvider);
      for (int i = 0; i < listIds.size(); i++) {
        if (i > 0)
          queryString.append(" or ");
        queryString.append("(exo:categoryId='").append(listIds.get(i)).append("')");
      }
      queryString.append("]order by @exo:createdDate ascending");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      QuestionPageList pageList = new QuestionPageList(result.getNodes(), 10, queryString.toString(), true);
      return pageList;
    } catch (Exception e) {
      log.error("Failed to get all questions: ", e);
    }
    return null;
  }

  @Override
  public QuestionPageList getQuestionsNotYetAnswer(String categoryId, boolean isApproved) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node categoryHome = getCategoryHome(sProvider, null);
      QueryManager qm = categoryHome.getSession().getWorkspace().getQueryManager();
      String qr = EMPTY_STR;
      boolean isOpenQs = false;
      if (categoryId.indexOf(" ") > 0) {
        String[] strs = categoryId.split(" ");
        categoryId = strs[0];
        if (strs.length == 3) {
          qr = strs[1];
          isOpenQs = Boolean.parseBoolean(strs[2]);
        } else {
          isOpenQs = Boolean.parseBoolean(strs[1]);
        }
      }
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(categoryHome.getPath()).append("//element(*,exo:faqQuestion)[");
      if (categoryId.equals(Utils.ALL)) {
        List<String> listIds = getViewableCategoryIds(sProvider);
        for (int i = 0; i < listIds.size(); i++) {
          if (i > 0)
            queryString.append(" or ");
          queryString.append("(exo:categoryId='").append(listIds.get(i)).append("')");
        }
      } else {
        queryString.append("((@exo:categoryId='").append(categoryId).append("')").append((categoryId.indexOf("/") > 0) ? (" or (@exo:categoryId='" + categoryId.substring(categoryId.lastIndexOf("/") + 1) + "'))") : ")");
      }
      if (isApproved)
        queryString.append(" and (@exo:isApproved='true')");
      if (qr.length() > 0)
        queryString.append(" and ((@exo:isApproved='true') or ").append(qr).append(")");
      queryString.append("] order by @exo:createdDate ascending");

      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      QuestionPageList pageList = new QuestionPageList(result.getNodes(), 10, queryString.toString(), true);
      pageList.setNotYetAnswered(true);
      pageList.setOpenQuestion(isOpenQs);
      return pageList;
    } catch (Exception e) {
      log.error("Get question not yet answer failed: ", e);
    }
    return null;
  }

  @Override
  public QuestionPageList getPendingQuestionsByCategory(String categoryId, FAQSetting faqSetting) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node categoryHome = getCategoryHome(sProvider, null);
      QueryManager qm = categoryHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = null;
      if (categoryId == null || categoryId.trim().length() < 1)
        categoryId = "null";
      queryString = new StringBuffer(JCR_ROOT).append(categoryHome.getPath())
                                              .append("//element(*,")
                                              .append(EXO_FAQ_QUESTION)
                                              .append(")[((")
                                              .append(AT)
                                              .append(EXO_CATEGORY_ID)
                                              .append("='")
                                              .append(categoryId)
                                              .append("')")
                                              .append((categoryId.indexOf("/") > 0) ? (" or (" + AT + EXO_CATEGORY_ID + "='" + categoryId.substring(categoryId.lastIndexOf("/") + 1) + "'))") : ")")
                                              .append(" and (")
                                              .append(AT)
                                              .append(EXO_IS_ACTIVATED)
                                              .append("='true') and (")
                                              .append(AT)
                                              .append(EXO_IS_APPROVED)
                                              .append("='false')]");
      queryString.append("order by ").append(Utils.getOderBy(faqSetting));
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      QuestionPageList pageList = new QuestionPageList(result.getNodes(), 10, queryString.toString(), true);
      return pageList;
    } catch (Exception e) {
      log.error("Get pedding question through category failed: ", e);
    }
    return null;
  }

  @Override
  public QuestionPageList getQuestionsByCatetory(String categoryId, FAQSetting faqSetting) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      if (CommonUtils.isEmpty(categoryId)) {
        categoryId = Utils.CATEGORY_HOME;
      }
      Node categoryNode = getCategoryNode(sProvider, categoryId);
      categoryId = categoryNode.getName();
      QueryManager qm = categoryNode.getSession().getWorkspace().getQueryManager();
      String userId = faqSetting.getCurrentUser();
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(categoryNode.getPath()).append("/").append(Utils.QUESTION_HOME).append("/element(*,")
                                 .append(EXO_FAQ_QUESTION).append(")[(@").append(EXO_CATEGORY_ID).append("='").append(categoryId)
                                 .append("') and (@").append(EXO_IS_ACTIVATED).append("='true')");
      if (!faqSetting.isCanEdit()) {
        queryString.append(" and (@").append(EXO_IS_APPROVED).append("='true'");
        if (userId != null && userId.length() > 0 && FAQSetting.DISPLAY_BOTH.equals(faqSetting.getDisplayMode())) {
          queryString.append(" or @").append(EXO_AUTHOR).append("='").append(userId).append("')");
        } else {
          queryString.append(")");
        }
      } else {
        if (FAQSetting.DISPLAY_APPROVED.equals(faqSetting.getDisplayMode())) {
          queryString.append(" and (@").append(EXO_IS_APPROVED).append("='true')");
        }
      }
      queryString.append("] order by ").append(Utils.getOderBy(faqSetting));
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      QuestionPageList pageList = new QuestionPageList(result.getNodes(), 10, queryString.toString(), true);
      return pageList;
    } catch (Exception e) {
      log.debug("Getting question through category failed: ", e);
    }
    return null;
  }

  @Override
  public QuestionPageList getAllQuestionsByCatetory(String categoryId, FAQSetting faqSetting) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node categoryHome = getCategoryHome(sProvider, null);
      QueryManager qm = categoryHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = null;
      if (FAQSetting.DISPLAY_APPROVED.equals(faqSetting.getDisplayMode())) {
        queryString = new StringBuffer(JCR_ROOT).append(categoryHome.getPath()).append("//element(*,exo:faqQuestion)[(@exo:categoryId='").append(categoryId).append("')").append(" and (@exo:isApproved='true')").append("]");
      } else {
        queryString = new StringBuffer(JCR_ROOT).append(categoryHome.getPath()).append("//element(*,exo:faqQuestion)[@exo:categoryId='").append(categoryId).append("'").append("]");
      }
      queryString.append("order by ").append(Utils.getOderBy(faqSetting));
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      QuestionPageList pageList = new QuestionPageList(result.getNodes(), 10, queryString.toString(), true);
      return pageList;
    } catch (Exception e) {
      log.debug("Failed to get all question through category: ", e);
    }
    return null;
  }
  
  @Override
  public List<String> getAllActivityIdsByCatetory(String categoryId) throws Exception {
    List<String> activityIds = new ArrayList<String>();
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node categoryNode = getCategoryNode(sProvider, categoryId);
      QueryManager qm = categoryNode.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer(JCR_ROOT);
      queryString.append(categoryNode.getPath()).append("//element(*,exo:faqQuestion)");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      NodeIterator result = query.execute().getNodes();
      
      while (result.hasNext()) {
        Node questionNode = result.nextNode();
        String activityId = ActivityTypeUtils.getActivityId(questionNode);
        if (CommonUtils.isEmpty(activityId) == false) {
          activityIds.add(activityId);
        }
      }
    } catch (Exception e) {
      log.debug("Failed to get all activity ids through category: ", e);
    }
    return activityIds;
  }

  @Override
  public QuestionPageList getQuestionsByListCatetory(List<String> listCategoryId, boolean isNotYetAnswer) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node categoryHome = getCategoryHome(sProvider, null);

      QueryManager qm = categoryHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(categoryHome.getPath()).append("//element(*,exo:faqQuestion) [");
      queryString.append(" (");
      int i = 0;
      for (String categoryId : listCategoryId) {
        if (i > 0)
          queryString.append(" or ");
        queryString.append("(@exo:categoryId='").append(categoryId).append("')");
        i++;
      }
      queryString.append(")]");
      queryString.append(" order by @exo:createdDate ascending");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      QuestionPageList pageList = null;
      pageList = new QuestionPageList(result.getNodes(), 10, queryString.toString(), true);
      pageList.setNotYetAnswered(isNotYetAnswer);
      return pageList;
    } catch (Exception e) {
      log.debug("Failed get questions through list of category: ", e);
    }
    return null;
  }

  @Override
  public List<Question> getQuickQuestionsByListCatetory(List<String> listCategoryId, boolean isNotYetAnswer) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    List<Question> questions = new ArrayList<Question>();
    try {
      Node categoryHome = getCategoryHome(sProvider, null);
      QueryManager qm = categoryHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(categoryHome.getPath()).append("//element(*,exo:faqQuestion)[(");
      int i = 0;
      for (String categoryId : listCategoryId) {
        if (i > 0)
          queryString.append(" or ");
        queryString.append("(@exo:categoryId='").append(categoryId).append("')");
        i++;
      }
      queryString.append(")]order by @exo:createdDate ascending");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator iter = result.getNodes();
      while (iter.hasNext()) {
        questions.add(getQuickQuestion(iter.nextNode()));
      }
    } catch (Exception e) {
      log.debug("Getting quick questions through list of category failed: ", e);
    }
    return questions;
  }

  private Question getQuickQuestion(Node questionNode) throws Exception {
    Question question = new Question();
    question.setId(questionNode.getName());
    PropertyReader reader = new PropertyReader(questionNode);
    question.setCategoryId(reader.string(EXO_CATEGORY_ID, EMPTY_STR));
    String path = questionNode.getPath();
    question.setPath(path.substring(path.indexOf(Utils.FAQ_APP) + Utils.FAQ_APP.length() + 1));
    question.setQuestion(reader.string(EXO_TITLE, EMPTY_STR));
    question.setApproved(reader.bool(EXO_IS_APPROVED));
    question.setActivated(reader.bool(EXO_IS_ACTIVATED));
    return question;
  }

  @Override
  public String getCategoryPathOfQuestion(String questionPath) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    StringBuilder pathName = new StringBuilder();
    Node faqHome = null;
    try {
      faqHome = getFAQServiceHome(sProvider);
      Node question = faqHome.getNode(questionPath);
      Node subCat = question.getParent().getParent();
      pathName.append(new PropertyReader(faqHome).string(EXO_NAME, "home"));
      while (!subCat.getName().equals(Utils.CATEGORY_HOME)) {
        pathName.append(CommonUtils.SLASH).append(new PropertyReader(subCat).string(EXO_NAME, subCat.getName()));
        subCat = subCat.getParent();
      }
    } catch (Exception e) {
      if (log.isDebugEnabled()) {
        log.debug("Getting category path of the question failed: ", e);
      }
    }
    return pathName.toString();
  }

  @Override
  public void moveQuestions(List<String> questions, String destCategoryId, String questionLink, FAQSetting faqSetting) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node faqHome = getFAQServiceHome(sProvider);
      String homePath = faqHome.getPath();
      Node destQuestionHome;
      try {
        destQuestionHome = (Node) faqHome.getNode(destCategoryId + "/" + Utils.QUESTION_HOME);
      } catch (Exception e) {
        destQuestionHome = faqHome.getNode(destCategoryId).addNode(Utils.QUESTION_HOME, EXO_FAQ_QUESTION_HOME);
        faqHome.getSession().save();
      }
      for (String id : questions) {
        try {
          Node destCateNode = faqHome.getNode(id).getParent();
          faqHome.getSession().move(homePath + "/" + id, destQuestionHome.getPath() + id.substring(id.lastIndexOf("/")));
          faqHome.getSession().save();
          Node questionNode = faqHome.getNode(destCategoryId + "/" + Utils.QUESTION_HOME + id.substring(id.lastIndexOf("/")));
          String catId = destCategoryId.substring(destCategoryId.lastIndexOf("/") + 1);
          questionNode.setProperty(EXO_CATEGORY_ID, catId);
          NodeIterator iter = questionNode.getNodes();
          Node attNode;
          while (iter.hasNext()) {
            attNode = iter.nextNode();
            if (attNode.isNodeType(EXO_FAQ_ATTACHMENT)) {
              attNode.getNode(JCR_CONTENT).setProperty(EXO_CATEGORY_ID, catId);
            }
          }
          updateDatas(questionNode, catId, true);
          updateDatas(questionNode, catId, false);
          questionNode.save();
          try {
            sendNotifyMoveQuestion(destCateNode, questionNode, catId, questionLink, faqSetting);
          } catch (Exception e) {
            log.warn("Failed to send notification of moved questions", e);
          }
        } catch (ItemNotFoundException ex) {
          log.warn(String.format("Destination category with %s node is not found", id), ex);
        }
      }
    } catch (Exception e) {
      log.error("Failed to remove question: ", e);
    }
  }

  private void sendNotifyMoveQuestion(Node destCateNode, Node questionNode, String cateId, String link, FAQSetting faqSetting) throws Exception {
    Node categoryName = questionNode.getParent().getParent();
    PropertyReader reader = new PropertyReader(categoryName);
    
    MessageBuilder messageBuilder = new MessageBuilder();
    messageBuilder.setType(MessageBuilder.TYPESEND.MOVE_QUESTION);
    messageBuilder.setCategoryName(reader.string(EXO_NAME, "Root"));
    messageBuilder.setContent(faqSetting.getEmailMoveQuestion());
    messageBuilder.setSubject(faqSetting.getEmailSettingSubject());
    
    reader = new PropertyReader(questionNode);
    messageBuilder.setQuestionOwner(reader.string(EXO_AUTHOR, EMPTY_STR));
    messageBuilder.setQuestionEmail(reader.string(EXO_EMAIL, EMPTY_STR));
    messageBuilder.setQuestionLink(link);
    messageBuilder.setQuestionDetail(reader.string(EXO_NAME, EMPTY_STR));
    messageBuilder.setQuestionContent(reader.string(EXO_TITLE, EMPTY_STR));
    
    Set<String> emails = new HashSet<String>();
    emails.addAll(calculateMoveEmail(destCateNode));
    emails.addAll(calculateMoveEmail(questionNode.getParent()));
    emails.add(messageBuilder.getQuestionEmail());
    sendEmailNotification(new ArrayList<String>(emails), messageBuilder.getMessage());
  }

  private Set<String> calculateMoveEmail(Node node) throws Exception {
    Set<String> set = new HashSet<String>();
    while (!node.getName().equals(Utils.CATEGORY_HOME)) {
      if (node.isNodeType(EXO_FAQ_WATCHING)) {
        set.addAll(new PropertyReader(node).list(EXO_EMAIL_WATCHING, new ArrayList<String>()));
      }
      node = node.getParent();
    }
    return set;
  }

  private void updateDatas(Node question, String catId, boolean isAnswer) throws Exception {
    try {
      QueryManager qm = question.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(question.getPath()).append("//element(*,").append((isAnswer) ? EXO_ANSWER : EXO_COMMENT).append(")");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator iter = result.getNodes();
      while (iter.hasNext()) {
        iter.nextNode().setProperty(EXO_CATEGORY_ID, catId);
      }
    } catch (Exception e) {
      log.error((isAnswer) ? "Updating answers failed: " : "Updating comments failed: ", e);
    }
  }

  @Override
  public void changeStatusCategoryView(List<String> listCateIds) throws Exception {
    if (listCateIds == null || listCateIds.size() < 1) {
      return;
    }
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      for (String id : listCateIds) {
        Node cat = getCategoryNode(sProvider, id);
        if (cat != null) {
          cat.setProperty(EXO_IS_VIEW, !cat.getProperty(EXO_IS_VIEW).getBoolean());
          cat.save();
        }
      }
    } catch (Exception e) {
      log.error("Changing status category view failed: ", e);
    }
  }

  @Override
  public long getMaxindexCategory(String parentId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    long max = 0;
    try {
      NodeIterator iter = getFAQServiceHome(sProvider).getNode((parentId == null) ? Utils.CATEGORY_HOME : parentId).getNodes();
      while (iter.hasNext()) {
        Node node = iter.nextNode();
        if (node.isNodeType(EXO_FAQ_CATEGORY))
          max = max + 1;
      }
    } catch (Exception e) {
      log.error("Failed to get max index category", e);
    }
    return max;
  }

  private void saveCategory(Node categoryNode, Category category, boolean isNew, SessionProvider sProvider) throws Exception {
    Map<String, String> moderators = new HashMap<String, String>();
    if (!categoryNode.getName().equals(Utils.CATEGORY_HOME)) {
      Node parentCategory = categoryNode.getParent();
      if (parentCategory.hasProperty(EXO_MODERATORS)) {
        for (Value vl : parentCategory.getProperty(EXO_MODERATORS).getValues()) {
          moderators.put(vl.getString(), vl.getString());
        }
      }
    }

    if (category.getId() != null) {
      categoryNode.setProperty(EXO_ID, category.getId());
      categoryNode.setProperty(EXO_CREATED_DATE, CommonUtils.getGreenwichMeanTime());
      categoryNode.setProperty(EXO_IS_VIEW, category.isView());
    }
    categoryNode.setProperty(EXO_INDEX, category.getIndex());
    categoryNode.setProperty(EXO_NAME, category.getName());
    categoryNode.setProperty(EXO_DESCRIPTION, category.getDescription());
    for (String mod : category.getModerators()) {
      moderators.put(mod, mod);
    }
    categoryNode.setProperty(EXO_MODERATORS, moderators.values().toArray(new String[] {}));
    categoryNode.setProperty(EXO_IS_MODERATE_QUESTIONS, category.isModerateQuestions());
    categoryNode.setProperty(EXO_VIEW_AUTHOR_INFOR, category.isViewAuthorInfor());
    categoryNode.setProperty(EXO_IS_MODERATE_ANSWERS, category.isModerateAnswers());
    categoryNode.setProperty(EXO_USER_PRIVATE, category.getUserPrivate());
    if (!isNew) {
      try {
        updateModeratorForChildCategories(categoryNode, moderators);
      } catch (Exception e) {
        log.debug("Updating moderator for child category failed: ", e);
      }
    }
    category.setPath(getCagoryPath(categoryNode.getPath()));
    if (categoryNode.isNew())
      categoryNode.getSession().save();
    else
      categoryNode.save();
  }

  private void updateModeratorForChildCategories(Node currentCategory, Map<String, String> moderators) throws Exception {
    Map<String, String> modMap = new HashMap<String, String>();
    Node cat;
    NodeIterator iter = currentCategory.getNodes();
    while (iter.hasNext()) {
      cat = iter.nextNode();
      if (cat.isNodeType(EXO_FAQ_CATEGORY)) {
        modMap.clear();
        modMap.putAll(moderators);
        for (Value vl : cat.getProperty(EXO_MODERATORS).getValues()) {
          modMap.put(vl.getString(), vl.getString());
        }
        cat.setProperty(EXO_MODERATORS, modMap.values().toArray(new String[] {}));
        cat.save();
        if (cat.hasNodes()) {
          updateModeratorForChildCategories(cat, modMap);
        }
      }
    }
  }

  private void resetIndex(Node goingCategory, long index, long gindex) throws Exception {
    if(index == gindex) {
      return;
    }
    Node parent = goingCategory.getParent();
    Node node;
    NodeIterator iter = getCategoriesIterator(parent);
    if (index <= iter.getSize()) {
      if (index < 1) {
        goingCategory.setProperty(EXO_INDEX, 1);
      }
      long l = 1;
      while (iter.hasNext()) {
        node = iter.nextNode();
        if (index < 1) {// move up to top
          if (node.getName().equals(goingCategory.getName()))
            continue;
          else {
            l++;
            node.setProperty(EXO_INDEX, l);
          }
        } else if (index > gindex) {// move down to index
          l = node.getProperty(EXO_INDEX).getLong();
          if (l >= gindex && l <= index) {
            if (l == gindex) {
              goingCategory.setProperty(EXO_INDEX, index);
            } else {
              node.setProperty(EXO_INDEX, l - 1);
            }
          }
          if (l > index)
            break;
        } else {// move up to index
          l = node.getProperty(EXO_INDEX).getLong();
          if (l > index) {
            if (l == gindex) {
              goingCategory.setProperty(EXO_INDEX, index + 1);
            } else {
              node.setProperty(EXO_INDEX, l + 1);
            }
          }
        }
      }
      parent.save();
    } else {
      goingCategory.setProperty(EXO_INDEX, iter.getSize());
      goingCategory.save();
    }
    reUpdateIndex(parent);
  }
  
  private void reUpdateIndex(Node parentCateNode) throws Exception {
    NodeIterator iter = getCategoriesIterator(parentCateNode);
    long i = 1;
    Node node;
    while (iter.hasNext()) {
      node = iter.nextNode();
      node.setProperty(EXO_INDEX, i);
      i++;
    }
    parentCateNode.save();
  }

  @Override
  public void saveCategory(String parentId, Category cat, boolean isAddNew){
    saveCategory(parentId, cat, isAddNew, (isAddNew && cat.getIndex() != 0 || isAddNew == false));
  }

  @Override
  public void saveCategory(String parentId, Category cat, boolean isAddNew, boolean isResetIndex){
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node newCategory;
      if (isAddNew) {
        Node parentNode = getFAQServiceHome(sProvider).getNode(parentId);
        newCategory = parentNode.addNode(cat.getId(), EXO_FAQ_CATEGORY);
        newCategory.addMixin(MIX_FAQ_SUB_CATEGORY);
        newCategory.addNode(Utils.QUESTION_HOME, EXO_FAQ_QUESTION_HOME);
        newCategory.getSession().save();
      } else {
        newCategory = getFAQServiceHome(sProvider).getNode(cat.getPath());
      }
      long index = cat.getIndex();
      long oldIndex = new PropertyReader(newCategory).l(EXO_INDEX, newCategory.getParent().getNodes().getSize());
      cat.setIndex(oldIndex);
      //
      saveCategory(newCategory, cat, isAddNew, sProvider);
      if (isResetIndex) {
        index = (index < oldIndex) ? index - 1 : index;
        resetIndex(newCategory, index, oldIndex);
      }
    } catch (Exception e) {
      log.error("Failed to save category: ", e);
    }
  }

  private List<Cate> listingSubTree(Node currentCategory, int i) throws Exception {
    Node cat;
    int j = i;
    j = j + 1;
    List<Cate> cateList = new ArrayList<Cate>();
    Cate cate;
    NodeIterator iter = currentCategory.getNodes();
    while (iter.hasNext()) {
      cat = iter.nextNode();
      if (cat.isNodeType(EXO_FAQ_CATEGORY)) {
        cate = new Cate();
        cate.setCategory(getCategory(cat));
        cate.setDeft(i);
        cateList.add(cate);
        if (cat.hasNodes()) {
          cateList.addAll(listingSubTree(cat, j));
        }
      }
    }
    return cateList;
  }

  @Override
  public List<Cate> listingCategoryTree() throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    Node cateHome = getCategoryHome(sProvider, null);
    int i = 1;
    List<Cate> cateList = new ArrayList<Cate>();
    cateList.addAll(listingSubTree(cateHome, i));
    return cateList;
  }
  
  private Category readeCategoryTree(Node categoryNode) throws Exception {
    Category cate = new Category();
    PropertyReader reader = new PropertyReader(categoryNode);
    cate.setId(categoryNode.getName());
    cate.setPath(categoryNode.getPath());
    cate.setName(reader.string(EXO_NAME));
    cate.setIndex(reader.l(EXO_INDEX));
    cate.setView(reader.bool(EXO_IS_VIEW));
    cate.setModerators(reader.strings(EXO_MODERATORS, new String[] {}));
    cate.setUserPrivate(reader.strings(EXO_USER_PRIVATE, new String[] {}));
    return cate;
  }
  
  public CategoryTree buildCategoryTree(String categoryId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    CategoryTree categoryTree = new CategoryTree();
    Node cateNode;
    if (CommonUtils.isEmpty(categoryId)) {
      cateNode = getCategoryHome(sProvider, null);
    } else {
      cateNode = getCategoryNode(sProvider, categoryId);
      if (cateNode == null)
        return null;
    }

    categoryTree.setCategory(readeCategoryTree(cateNode));
    categoryTree.setSubCategory(getCategoryTree(cateNode));

    return categoryTree;
  }
  
  private List<CategoryTree> getCategoryTree(Node categoryNode) throws Exception {
    List<CategoryTree> categoryTrees = new ArrayList<CategoryTree>();
    NodeIterator iter = categoryNode.getNodes();
    Node cateNode;
    CategoryTree categoryTree;
    while (iter.hasNext()) {
      cateNode = iter.nextNode();
      if (cateNode.isNodeType(EXO_FAQ_CATEGORY)) {
        categoryTree = new CategoryTree();
        categoryTree.setCategory(readeCategoryTree(cateNode));
        categoryTree.setSubCategory(getCategoryTree(cateNode));
        categoryTrees.add(categoryTree);
      }
    }
    return categoryTrees;
  }

  @Override
  public void removeCategory(String categoryId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node faqHome = getFAQServiceHome(sProvider);
      Node node = faqHome.getNode(categoryId);
      Node parent = node.getParent();
      node.remove();
      faqHome.save();
      // update index
      reUpdateIndex(parent);
    } catch (Exception e) {
      log.error("Can not remove category has id: " + categoryId);
    }
  }

  private Category getCategory(Node categoryNode) throws Exception {
    if (categoryNode == null)
      return null;
    Category category = new Category();
    PropertyReader reader = new PropertyReader(categoryNode);
    category.setId(categoryNode.getName());
    category.setName(reader.string(EXO_NAME, EMPTY_STR));
    category.setDescription(reader.string(EXO_DESCRIPTION, EMPTY_STR));
    category.setCreatedDate(reader.date(EXO_CREATED_DATE));
    category.setModerators(reader.strings(EXO_MODERATORS, new String[] {}));
    category.setUserPrivate(reader.strings(EXO_USER_PRIVATE, new String[] {}));
    category.setModerateQuestions(reader.bool(EXO_IS_MODERATE_QUESTIONS));
    category.setModerateAnswers(reader.bool(EXO_IS_MODERATE_ANSWERS));
    category.setViewAuthorInfor(reader.bool(EXO_VIEW_AUTHOR_INFOR));
    category.setIndex(reader.l(EXO_INDEX, 1));
    category.setView(reader.bool(EXO_IS_VIEW, true));
    category.setPath(getCagoryPath(categoryNode.getPath()));
    return category;
  }
  
  private String getCagoryPath(String path) {
    return path.substring(path.indexOf(Utils.FAQ_APP) + Utils.FAQ_APP.length() + 1);
  }

  @Override
  public Category getCategoryById(String categoryId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      return getCategory(getCategoryNode(sProvider, categoryId));
    } catch (Exception e) {
      log.debug("Category not found " + categoryId);
    }
    return null;
  }

  @Override
  public List<Category> findCategoriesByName(String categoryName) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node categoryHome = getCategoryHome(sProvider, null);
      QueryManager qm = categoryHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(categoryHome.getPath()).append("//element(*,exo:faqCategory)[@exo:name='").append(categoryName).append("']");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult queryResult = query.execute();
      NodeIterator iter = queryResult.getNodes();
      List<Category> result = new ArrayList<Category>();
      while (iter.hasNext()) {
        result.add(getCategory(iter.nextNode()));
      }
      return result;
    } catch (Exception e) {
      log.error("Could not retrieve categories by name " + categoryName, e);
    }
    return null;
  }

  @Override
  public List<String> getListCateIdByModerator(String user) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      List<String> allOfUser = UserHelper.getAllGroupAndMembershipOfUser(user);
      StringBuilder builder = new StringBuilder();      
      int i = 0;
      if(allOfUser != null && allOfUser.size() > 0){
        builder.append(" ( ");
        for (String strUser : allOfUser) {
          strUser = strUser.trim();
          if (strUser.length() > 0) {
            if (i > 0) {
              builder.append(" or ");
            }
            builder.append("@").append(EXO_MODERATORS).append("='").append(strUser).append("'");
            i++;
          }
        }
        builder.append(" ) and ");
      }
      builder.append("(@").append(EXO_IS_VIEW).append("='true')");
      Node categoryHome = getCategoryHome(sProvider, null);
      QueryManager qm = categoryHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(categoryHome.getParent().getPath())
                                                           .append("//element(*,").append(EXO_FAQ_CATEGORY).append(")[").append(builder).append("]");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator iter = result.getNodes();
      List<String> listCateId = new ArrayList<String>();
      while (iter.hasNext()) {
        Node cate = iter.nextNode();
        try {
          listCateId.add(cate.getName() + CommonUtils.SEMICOLON + cate.getProperty(EXO_NAME).getString());
        } catch (Exception e) {
          log.debug("Getting property of " + cate + " node failed: ", e);
        }
      }
      return listCateId;
    } catch (Exception e) {
      log.error("Failed to get list of CateID through Moderator: ", e);
    }
    return null;
  }

  @Override
  public List<Category> getAllCategories() throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node categoryHome = getCategoryHome(sProvider, null);
      QueryManager qm = categoryHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(categoryHome.getPath()).append("//element(*,exo:faqCategory)");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator iter = result.getNodes();
      List<Category> catList = new ArrayList<Category>();
      while (iter.hasNext()) {
        catList.add(getCategory(iter.nextNode()));
      }
      return catList;
    } catch (Exception e) {
      log.error("Getting all category failed: ", e);
    }
    return null;

  }
  
  public Object readQuestionProperty(String questionId, String propertyName, Class returnType) throws Exception{
    if (questionId == null || propertyName == null) 
      throw new IllegalArgumentException("The parameter is null");
    Node questionNode = getQuestionNodeById(questionId);
    if (questionNode != null)
      return new PropertyReader(questionNode).readProperty(propertyName, returnType);
    else return null;
  }
  
  /**
   * read property of the category by its name
   * @param categoryId id of the category
   * @param propertyName name of the property
   * @param returnValue expected return-type. The supported class types are String[], String, Long, Boolean, Double and Date.  
   * @return 
   * @throws Exception
   */
  public Object readCategoryProperty(String categoryId, String propertyName, Class returnType) throws Exception {
    if (categoryId == null || propertyName == null) 
      throw new IllegalArgumentException("The parameter is null");
    Node categoryNode = getCategoryNodeById(categoryId);
    if (categoryNode != null)
      return new PropertyReader(categoryNode).readProperty(propertyName, returnType);
    else return null;
  }
  
  public long existingCategories() throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node categoryHome = getCategoryHome(sProvider, null);
      QueryManager qm = categoryHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(categoryHome.getPath()).append("//element(*,exo:faqCategory)");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      result.getNodes().getSize();
    } catch (Exception e) {
      log.error("Failed to check existing categories", e);
    }
    return 0;
  }

  public Node getCategoryNodeById(String categoryId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      return getCategoryNode(sProvider, categoryId);
    } catch (Exception e) {
      log.error("Getting node failed: ", e);
    }
    return null;
  }

  /**
   * Get node category by categoryId or category full path or relative path.
   * 
   * @param: sProvider the SessionProvider.
   * @param: param the category id or category relative path.
   * @return: The Node has node type is exo:faqCategory. 
   */
  private Node getCategoryNode(SessionProvider sProvider, String param) throws Exception {
    try {
      Node faqHome = getFAQServiceHome(sProvider);
      try {
        return (Node) faqHome.getSession().getItem(param);
      } catch (RepositoryException e) {
        return faqHome.getNode(param);
      }
    } catch (PathNotFoundException e) {
      param = (param.indexOf("/") > 0) ? param.substring(param.lastIndexOf("/") + 1) : param;
      Node categoryHome = getCategoryHome(sProvider, null);
      QueryManager qm = categoryHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(categoryHome.getPath()).append("//element(*,").append(EXO_FAQ_CATEGORY).append(")").append("[fn:name()='").append(param).append("']");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator iter = result.getNodes();
      if (iter.getSize() != 0)
        return iter.nextNode();
    }
    return null;
  }

  @Override
  public List<Category> getSubCategories(String categoryId, FAQSetting faqSetting, boolean isGetAll, List<String> limitedUsers) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    List<Category> catList = new ArrayList<Category>();
    if (limitedUsers == null) {
      limitedUsers = UserHelper.getAllGroupAndMembershipOfUser(null);
    }
    try {
      Node parentCategory;
      if (categoryId == null || categoryId.equals(Utils.CATEGORY_HOME)) {
        parentCategory = getCategoryHome(sProvider, null);
      } else
        parentCategory = getFAQServiceHome(sProvider).getNode(categoryId);
      if (!faqSetting.isAdmin()) {
        PropertyReader reader = new PropertyReader(parentCategory);
        List<String> userPrivates = reader.list(EXO_USER_PRIVATE, new ArrayList<String>());
        if (!userPrivates.isEmpty() && !Utils.hasPermission(userPrivates, limitedUsers))
          return catList;
      }

      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(parentCategory.getPath());
      if (faqSetting.isAdmin()) {
        queryString.append("/element(*,").append(EXO_FAQ_CATEGORY)
                   .append(") [@").append(EXO_IS_VIEW).append("='true'] order by @").append(EXO_INDEX).append(" ascending");
      } else {
        queryString.append("/element(*,").append(EXO_FAQ_CATEGORY)
                   .append(") [@").append(EXO_IS_VIEW).append("='true' and ( not(@").append(EXO_USER_PRIVATE)
                   .append(") or @").append(EXO_USER_PRIVATE).append("=''");
        if (limitedUsers.size() > 0) {
          queryString.append(" or ").append(Utils.buildQueryListOfUser(EXO_USER_PRIVATE, limitedUsers));
          queryString.append(" or ").append(Utils.buildQueryListOfUser(EXO_MODERATORS, limitedUsers));
        }
        queryString.append(")] order by @").append(EXO_INDEX).append(" ascending");
      }
      QueryManager qm = parentCategory.getSession().getWorkspace().getQueryManager();
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator iter = result.getNodes();
      while (iter.hasNext()) {
        catList.add(getCategory(iter.nextNode()));
      }
    } catch (Exception e) {
      throw e;
    }
    return catList;
  }

  @Override
  public long[] getCategoryInfo(String categoryId, FAQSetting faqSetting) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    long[] cateInfo = new long[] { 0, 0, 0, 0 };// categories, all, open, pending
    try {
      Node parentCategory;
      String id;
      parentCategory = getFAQServiceHome(sProvider).getNode(categoryId);
      if (categoryId.indexOf("/") > 0)
        id = categoryId.substring(categoryId.lastIndexOf("/") + 1);
      else
        id = categoryId;
      NodeIterator iter = parentCategory.getNodes();
      cateInfo[0] = iter.getSize();
      QueryManager qm = parentCategory.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(parentCategory.getPath()).append("//element(*,exo:faqQuestion)[(@exo:categoryId='").append(id).append("') and (@exo:isActivated='true')").append("]").append("order by @exo:createdDate ascending");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator nodeIterator = result.getNodes();
      cateInfo[1] = nodeIterator.getSize();// all

      Node questionNode = null;
      boolean onlyGetApproved, questionIsApproved = true, isShow = true;
      onlyGetApproved = (FAQSetting.DISPLAY_APPROVED.equals(faqSetting.getDisplayMode()));
      while (nodeIterator.hasNext()) {
        questionNode = nodeIterator.nextNode();
        questionIsApproved = questionNode.getProperty(EXO_IS_APPROVED).getBoolean();
        isShow = (questionIsApproved || ((faqSetting.isCanEdit() || questionNode.getProperty(EXO_AUTHOR).getString().equals(faqSetting.getCurrentUser())) && !onlyGetApproved));
        if (!questionIsApproved) {
          cateInfo[3]++;// pending
          if (!isShow)
            cateInfo[1]--;
        }
        if (isShow) {
          if (!hasAnswerInQuestion(qm, questionNode))
            cateInfo[2]++;// open
        }
      }
    } catch (Exception e) {
      log.error("Failed to get category info: ", e);
    }
    return cateInfo;
  }

  private boolean hasAnswerInQuestion(QueryManager qm, Node questionNode) throws Exception {
    StringBuffer queryString = new StringBuffer(JCR_ROOT).append(questionNode.getPath()).append("//element(*,exo:answer)[(@exo:approveResponses='true') and (@exo:activateResponses='true')] order by @exo:dateResponse ascending");
    QueryImpl impl = (QueryImpl) qm.createQuery(queryString.toString(), Query.XPATH);
    impl.setOffset(0);
    impl.setLimit(1);
    QueryResult result = impl.execute();
    NodeIterator iter = result.getNodes();
    return (iter.getSize() > 0) ? true : false;
  }

  @Override
  public void moveCategory(String categoryId, String destCategoryId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node faqHome = getFAQServiceHome(sProvider);
      Node srcNode = faqHome.getNode(categoryId);
      String destPath = faqHome.getPath() + "/" + destCategoryId + "/" + srcNode.getName();
      
      if(srcNode.getPath().equals(destPath)) return;
      
      Node parentNode = srcNode.getParent();
      String srcPath = srcNode.getPath();
      faqHome.getSession().move(srcPath, destPath);
      faqHome.getSession().save();
      Node destNode = faqHome.getNode(destCategoryId + "/" + srcNode.getName());
      destNode.setProperty(EXO_INDEX, destNode.getParent().getNodes().getSize());
      destNode.save();
      // update index for children categories of parent category moved. 
      reUpdateIndex(parentNode);
    } catch (ItemExistsException e) {
      throw e;
    } catch (Exception e) {
      if (log.isDebugEnabled())
        log.debug(e.getMessage());
    }
  }

  @Override
  public void addWatchCategory(String id, Watch watch) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node faqHome = getFAQServiceHome(sProvider);
      Map<String, String> watchs = new HashMap<String, String>();
      Node watchingNode = faqHome.getNode(id);
      if (watchingNode.isNodeType(EXO_FAQ_WATCHING)) {
        Value[] emails = watchingNode.getProperty(EXO_EMAIL_WATCHING).getValues();
        Value[] users = watchingNode.getProperty(EXO_USER_WATCHING).getValues();
        if (emails != null && users != null) {
          for (int i = 0; i < users.length; i++) {
            watchs.put(users[i].getString(), emails[i].getString());
          }
        }
        watchs.put(watch.getUser(), watch.getEmails());
        watchingNode.setProperty(EXO_EMAIL_WATCHING, watchs.values().toArray(new String[] {}));
        watchingNode.setProperty(EXO_USER_WATCHING, watchs.keySet().toArray(new String[] {}));
      } else {
        watchingNode.addMixin(EXO_FAQ_WATCHING);
        watchingNode.setProperty(EXO_EMAIL_WATCHING, new String[] { watch.getEmails() });
        watchingNode.setProperty(EXO_USER_WATCHING, new String[] { watch.getUser() });
      }
      watchingNode.save();
    } catch (Exception e) {
      log.error("Failed to add watch category: ", e);
    }
  }

  // TODO Going to remove
  @Override
  public QuestionPageList getListMailInWatch(String categoryId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node categoryHome = getCategoryHome(sProvider, null);
      QueryManager qm = categoryHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(categoryHome.getPath()).append("//element(*,exo:faqCategory)[@exo:id='").append(categoryId).append("']");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      QuestionPageList pageList = new QuestionPageList(result.getNodes(), 5, queryString.toString(), true);
      return pageList;
    } catch (Exception e) {
      log.error("Failed to get list of mail watch: ", e);
    }
    return null;
  }

  @Override
  public List<Watch> getWatchByCategory(String categoryId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    List<Watch> listWatches = new ArrayList<Watch>();
    try {
      Node category = getFAQServiceHome(sProvider).getNode(categoryId);
      if (category.isNodeType(EXO_FAQ_WATCHING)) {
        PropertyReader reader = new PropertyReader(category);
        String[] userWatch = reader.strings(EXO_USER_WATCHING);
        String[] emails = reader.strings(EXO_EMAIL_WATCHING);
        if (userWatch != null && userWatch.length > 0) {
          Watch watch;
          for (int i = 0; i < userWatch.length; i++) {
            watch = new Watch();
            watch.setEmails(emails[i]);
            watch.setUser(userWatch[i]);
            listWatches.add(watch);
          }
        }
      }
      return listWatches;
    } catch (Exception e) {
      log.error("Failed to get watch through category: ", e);
    }
    return listWatches;
  }

  @Override
  public boolean hasWatch(String categoryPath) {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node cat = getFAQServiceHome(sProvider).getNode(categoryPath);
      if (new PropertyReader(cat).strings(EXO_USER_WATCHING, new String[] {}).length > 0)
        return true;
    } catch (Exception e) {
      log.error("Failed to check has watch", e);
    }
    return false;
  }

  @Override
  public void addWatchQuestion(String questionId, Watch watch, boolean isNew) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    Map<String, String> watchMap = new HashMap<String, String>();
    try {
      Node questionNode = getFAQServiceHome(sProvider).getNode(questionId);
      if (questionNode.isNodeType(EXO_FAQ_WATCHING)) {
        Value[] values = questionNode.getProperty(EXO_EMAIL_WATCHING).getValues();
        Value[] users = questionNode.getProperty(EXO_USER_WATCHING).getValues();
        for (int i = 0; i < users.length; i++) {
          watchMap.put(users[i].getString(), values[i].getString());
        }
        watchMap.put(watch.getUser(), watch.getEmails());

        questionNode.setProperty(EXO_EMAIL_WATCHING, watchMap.values().toArray(new String[] {}));
        questionNode.setProperty(EXO_USER_WATCHING, watchMap.keySet().toArray(new String[] {}));
        questionNode.save();
      } else {
        questionNode.addMixin(EXO_FAQ_WATCHING);
        questionNode.setProperty(EXO_EMAIL_WATCHING, new String[] { watch.getEmails() });
        questionNode.setProperty(EXO_USER_WATCHING, new String[] { watch.getUser() });
        questionNode.save();
      }
    } catch (Exception e) {
      log.error("Failed to add a watch question: ", e);
    }
  }

  @Override
  public List<Watch> getWatchByQuestion(String questionId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    List<Watch> listWatches = new ArrayList<Watch>();
    try {
      Node quetionNode = getFAQServiceHome(sProvider).getNode(questionId);
      if (quetionNode.isNodeType(EXO_FAQ_WATCHING)) {
        PropertyReader reader = new PropertyReader(quetionNode);
        String[] userWatch = reader.strings(EXO_EMAIL_WATCHING);
        String[] emails = reader.strings(EXO_USER_WATCHING);
        if (userWatch != null && userWatch.length > 0) {
          Watch watch;
          for (int i = 0; i < userWatch.length; i++) {
            watch = new Watch();
            watch.setEmails(emails[i]);
            watch.setUser(userWatch[i]);
            listWatches.add(watch);
          }
        }
      }
    } catch (Exception e) {
      log.error("Failed to get watch through question: ", e);
    }
    return listWatches;
  }

  @Override
  public QuestionPageList getWatchedCategoryByUser(String userId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node categoryHome = getCategoryHome(sProvider, null);
      QueryManager qm = categoryHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = null;
      queryString = new StringBuffer(JCR_ROOT).append(categoryHome.getPath()).append("//element(*,exo:faqCategory)[(@exo:userWatching='").append(userId).append("')]");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      QuestionPageList pageList = new QuestionPageList(result.getNodes(), 10, queryString.toString(), true);
      return pageList;
    } catch (Exception e) {
      log.error("Failed to get watched category through user: ", e);
    }
    return null;
  }

  @Override
  public boolean isUserWatched(String userId, String cateId) {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node faqHome = getFAQServiceHome(sProvider);
      Node cate = faqHome.getNode(cateId);
      List<String> list = new PropertyReader(cate).list(EXO_USER_WATCHING, new ArrayList<String>());
      for (String vl : list) {
        if (vl.equals(userId))
          return true;
      }
    } catch (Exception e) {
      log.error("Failed to check user watched", e);
    }
    return false;
  }

  @Override
  public List<String> getWatchedSubCategory(String userId, String cateId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    List<String> watchedSub = new ArrayList<String>();
    try {
      Node faqHome = getFAQServiceHome(sProvider);
      Node category = faqHome.getNode(cateId);
      QueryManager qm = faqHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(category.getPath()).append("/element(*,exo:faqCategory)[(@exo:userWatching='").append(userId).append("')]");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator iter = result.getNodes();
      while (iter.hasNext()) {
        watchedSub.add(iter.nextNode().getName());
      }
    } catch (Exception e) {
      log.error("Getting watched sub category failed: ", e);
    }
    return watchedSub;
  }

  @Override
  public QuestionPageList getListQuestionsWatch(FAQSetting faqSetting, String currentUser) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node categoryHome = getCategoryHome(sProvider, null);
      QueryManager qm = categoryHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(categoryHome.getPath()).append("//element(*,exo:faqQuestion)[(@exo:userWatching='").append(currentUser).append("')");
      if (FAQSetting.DISPLAY_APPROVED.equals(faqSetting.getDisplayMode())) {
        queryString.append(" and (@exo:isApproved='true')");
      }
      if (!faqSetting.isAdmin())
        queryString.append(" and (@exo:isActivated='true')");
      queryString.append("] order by ").append(Utils.getOderBy(faqSetting));
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      QuestionPageList pageList = new QuestionPageList(result.getNodes(), 10, queryString.toString(), true);
      return pageList;
    } catch (Exception e) {
      log.error("Failed to get list of question watch: ", e);
    }
    return null;
  }

  // Going to remove
  @Override
  public void deleteCategoryWatch(String categoryId, String user) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node category = getFAQServiceHome(sProvider).getNode(categoryId);
      Map<String, String> emailMap = new HashMap<String, String>();
      Value[] emailValues = category.getProperty(EXO_EMAIL_WATCHING).getValues();
      Value[] userValues = category.getProperty(EXO_USER_WATCHING).getValues();
      for (int i = 0; i < emailValues.length; i++) {
        emailMap.put(userValues[i].getString(), emailValues[i].getString());
      }
      emailMap.remove(user);
      category.setProperty(EXO_USER_WATCHING, emailMap.keySet().toArray(new String[] {}));
      category.setProperty(EXO_EMAIL_WATCHING, emailMap.values().toArray(new String[] {}));
      category.save();
    } catch (Exception e) {
      log.error("Failed to deleted category watch", e);
    }
  }

  @Override
  public void unWatchCategory(String categoryId, String user) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node category = getFAQServiceHome(sProvider).getNode(categoryId);
      Map<String, String> userMap = new HashMap<String, String>();
      Value[] emailValues = category.getProperty(EXO_EMAIL_WATCHING).getValues();
      Value[] userValues = category.getProperty(EXO_USER_WATCHING).getValues();
      for (int i = 0; i < userValues.length; i++) {
        userMap.put(userValues[i].getString(), emailValues[i].getString());
      }
      userMap.remove(user);
      category.setProperty(EXO_EMAIL_WATCHING, userMap.values().toArray(new String[] {}));
      category.setProperty(EXO_USER_WATCHING, userMap.keySet().toArray(new String[] {}));
      category.save();
    } catch (Exception e) {
      log.error("Failed to unWatch category", e);
    }
  }

  @Override
  public void unWatchQuestion(String questionId, String user) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node question = getFAQServiceHome(sProvider).getNode(questionId);
      Map<String, String> userMap = new HashMap<String, String>();
      Value[] emailValues = question.getProperty(EXO_EMAIL_WATCHING).getValues();
      Value[] userValues = question.getProperty(EXO_USER_WATCHING).getValues();
      for (int i = 0; i < userValues.length; i++) {
        userMap.put(userValues[i].getString(), emailValues[i].getString());
      }
      userMap.remove(user);
      question.setProperty(EXO_EMAIL_WATCHING, userMap.values().toArray(new String[] {}));
      question.setProperty(EXO_USER_WATCHING, userMap.keySet().toArray(new String[] {}));
      question.save();
    } catch (Exception e) {
      log.error("Unwatching question failed: ", e);
    }
  }

  @Override
  public List<ObjectSearchResult> getSearchResults(FAQEventQuery eventQuery) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();

    eventQuery.setViewingCategories(getViewableCategoryIds(sProvider));
    List<String> retrictedCategoryList = new ArrayList<String>();
    if (!eventQuery.isAdmin())
      retrictedCategoryList = getRetrictedCategories(eventQuery.getUserId(), eventQuery.getUserMembers());

    Node categoryHome = getCategoryHome(sProvider, null);
    eventQuery.setPath(categoryHome.getPath());
    try {
      QueryManager qm = categoryHome.getSession().getWorkspace().getQueryManager();
      
      Query query = qm.createQuery(eventQuery.getQuery(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator iter = result.getNodes();
      Node nodeObj = null;
      if (eventQuery.getType().equals(FAQEventQuery.FAQ_CATEGORY)) { // Category search
        List<ObjectSearchResult> results = new ArrayList<ObjectSearchResult>();
        while (iter.hasNext()) {
          if (eventQuery.isAdmin()) {
            Node cat = iter.nextNode();
            // for retricted audiences
            if (retrictedCategoryList.size() > 0) {
              String path = cat.getPath();
              for (String id : retrictedCategoryList) {
                if (path.indexOf(id) > 0) {
                  results.add(getResultObj(cat));
                  break;
                }
              }
            } else {
              results.add(getResultObj(cat));
            }
          } else {
            results.add(getResultObj(iter.nextNode()));
          }

        }
        return results;
      } else if (eventQuery.getType().equals(FAQEventQuery.FAQ_QUESTION)) { // Question search
        List<ObjectSearchResult> results = new ArrayList<ObjectSearchResult>();
        Map<String, Node> mergeQuestion = new HashMap<String, Node>();
        Map<String, Node> mergeQuestion2 = new HashMap<String, Node>();
        List<Node> listQuestion = new ArrayList<Node>();
        List<Node> listLanguage = new ArrayList<Node>();
        Map<String, Node> listAnswerandComment = new HashMap<String, Node>();
        while (iter.hasNext()) {
          nodeObj = iter.nextNode();
          if (!eventQuery.isAdmin()) {
            try {
              if (nodeObj.isNodeType(EXO_FAQ_QUESTION)) {
                if ((nodeObj.getProperty(EXO_IS_APPROVED).getBoolean() == true && nodeObj.getProperty(EXO_IS_ACTIVATED).getBoolean() == true) || (nodeObj.getProperty(EXO_AUTHOR).getString().equals(eventQuery.getUserId()) && nodeObj.getProperty(EXO_IS_ACTIVATED).getBoolean() == true))
                  // for retricted audiences
                  if (retrictedCategoryList.size() > 0) {
                    String path = nodeObj.getPath();
                    boolean isCanView = true;
                    for (String id : retrictedCategoryList) {
                      if (path.indexOf(id) > 0) {
                        isCanView = false;
                        break;
                      }
                    }
                    if (isCanView)
                      listQuestion.add(nodeObj);
                  } else {
                    listQuestion.add(nodeObj);
                  }

              }

              if (nodeObj.isNodeType(EXO_FAQ_RESOURCE)) {
                Node nodeQuestion = nodeObj.getParent().getParent();
                if ((nodeQuestion.getProperty(EXO_IS_APPROVED).getBoolean() == true && nodeQuestion.getProperty(EXO_IS_ACTIVATED).getBoolean() == true) || (nodeQuestion.getProperty(EXO_AUTHOR).getString().equals(eventQuery.getUserId()) && nodeQuestion.getProperty(EXO_IS_ACTIVATED).getBoolean() == true))
                  // for retricted audiences
                  if (retrictedCategoryList.size() > 0) {
                    boolean isCanView = true;
                    String path = nodeObj.getPath();
                    for (String id : retrictedCategoryList) {
                      if (path.indexOf(id) > 0) {
                        isCanView = false;
                        break;
                      }
                    }
                    if (isCanView)
                      listQuestion.add(nodeQuestion);
                  } else {
                    listQuestion.add(nodeQuestion);
                  }

              }

              if (nodeObj.isNodeType(EXO_FAQ_LANGUAGE)) {
                Node nodeQuestion = nodeObj.getParent().getParent();
                if ((nodeQuestion.getProperty(EXO_IS_APPROVED).getBoolean() == true && nodeQuestion.getProperty(EXO_IS_ACTIVATED).getBoolean() == true) || (nodeQuestion.getProperty(EXO_AUTHOR).getString().equals(eventQuery.getUserId()) && nodeQuestion.getProperty(EXO_IS_ACTIVATED).getBoolean() == true))
                  // for retricted audiences
                  if (retrictedCategoryList.size() > 0) {
                    boolean isCanView = true;
                    String path = nodeObj.getPath();
                    for (String id : retrictedCategoryList) {
                      if (path.indexOf(id) > 0) {
                        isCanView = false;
                        break;
                      }
                    }
                    if (isCanView)
                      listLanguage.add(nodeObj);
                  } else {
                    listLanguage.add(nodeObj);
                  }
              }

              if (nodeObj.isNodeType(EXO_ANSWER) || nodeObj.isNodeType(EXO_COMMENT)) { // answers of default language
                String quesId = nodeObj.getProperty(EXO_QUESTION_ID).getString();
                if (!listAnswerandComment.containsKey(quesId)) {
                  Node nodeQuestion = nodeObj.getParent().getParent();
                  if (nodeQuestion.isNodeType(EXO_FAQ_QUESTION)) {
                    if ((nodeQuestion.getProperty(EXO_IS_APPROVED).getBoolean() == true && nodeQuestion.getProperty(EXO_IS_ACTIVATED).getBoolean() == true) || (nodeQuestion.getProperty(EXO_AUTHOR).getString().equals(eventQuery.getUserId()) && nodeQuestion.getProperty(EXO_IS_ACTIVATED).getBoolean() == true))
                      // for retricted audiences
                      if (retrictedCategoryList.size() > 0) {
                        boolean isCanView = true;
                        String path = nodeObj.getPath();
                        for (String id : retrictedCategoryList) {
                          if (path.indexOf(id) > 0) {
                            isCanView = false;
                            break;
                          }
                        }
                        if (isCanView)
                          listAnswerandComment.put(quesId, nodeObj);
                      } else {
                        listAnswerandComment.put(quesId, nodeObj);
                      }
                  } else { // answers of other languages
                    nodeQuestion = nodeObj.getParent().getParent().getParent().getParent();
                    if ((nodeQuestion.getProperty(EXO_IS_APPROVED).getBoolean() == true && nodeQuestion.getProperty(EXO_IS_ACTIVATED).getBoolean() == true) || (nodeQuestion.getProperty(EXO_AUTHOR).getString().equals(eventQuery.getUserId()) && nodeQuestion.getProperty(EXO_IS_ACTIVATED).getBoolean() == true)) {
                      // for retricted audiences
                      if (retrictedCategoryList.size() > 0) {
                        boolean isCanView = true;
                        String path = nodeObj.getPath();
                        for (String id : retrictedCategoryList) {
                          if (path.indexOf(id) > 0) {
                            isCanView = false;
                            break;
                          }
                        }
                        if (isCanView)
                          listAnswerandComment.put(quesId, nodeObj);
                      } else {
                        listAnswerandComment.put(quesId, nodeObj);
                      }
                    }
                  }

                }

              }
            } catch (Exception e) {
              log.error("Failed to add item in list search", e);
            }

          } else {
            if (nodeObj.isNodeType(EXO_FAQ_QUESTION))
              listQuestion.add(nodeObj);
            if (nodeObj.isNodeType(EXO_FAQ_RESOURCE))
              listQuestion.add(nodeObj.getParent().getParent());
            if (nodeObj.isNodeType(EXO_FAQ_LANGUAGE))
              listLanguage.add(nodeObj);
            if (nodeObj.isNodeType(EXO_ANSWER) || nodeObj.isNodeType(EXO_COMMENT))
              listAnswerandComment.put(nodeObj.getProperty(EXO_QUESTION_ID).getString(), nodeObj);
          }
        }

        boolean isInitiated = false;
        if (eventQuery.isQuestionLevelSearch()) {
          // directly return because there is only one this type of search
          if (!eventQuery.isLanguageLevelSearch() && !eventQuery.isAnswerCommentLevelSearch()) {
            List<String> list = new ArrayList<String>();
            for (Node node : listQuestion) {
              if (list.contains(node.getName()))
                continue;
              else
                list.add(node.getName());
              results.add(getResultObj(node));
            }
            return results;
          }
          // merging results
          if (!listQuestion.isEmpty()) {
            isInitiated = true;
            for (Node node : listQuestion) {
              mergeQuestion.put(node.getName(), node);
            }
          }
        }
        if (eventQuery.isLanguageLevelSearch()) {
          // directly return because there is only one this type of search
          if (!eventQuery.isQuestionLevelSearch() && !eventQuery.isAnswerCommentLevelSearch()) {
            for (Node node : listLanguage) {
              results.add(getResultObj(node));
            }
            return results;
          }

          // merging results
          if (isInitiated) {
            for (Node node : listLanguage) {
              String id = node.getProperty(EXO_QUESTION_ID).getString();
              if (mergeQuestion.containsKey(id)) {
                mergeQuestion2.put(id, mergeQuestion.get(id));
              }
            }
          } else {
            for (Node node : listLanguage) {
              mergeQuestion2.put(node.getProperty(EXO_QUESTION_ID).getString(), node);
            }
            isInitiated = true;
          }
        }

        if (eventQuery.isAnswerCommentLevelSearch()) {
          // directly return because there is only one this type of search
          if (!eventQuery.isLanguageLevelSearch() && !eventQuery.isQuestionLevelSearch()) {
            for (Node node : listAnswerandComment.values()) {
              results.add(getResultObj(node));
            }
            return results;
          }
          // merging results
          if (isInitiated) {
            if (eventQuery.isLanguageLevelSearch()) {
              if (mergeQuestion2.isEmpty())
                return results;
              for (Node node : listAnswerandComment.values()) {
                String id = node.getProperty(EXO_QUESTION_ID).getString();
                if (mergeQuestion2.containsKey(id)) {
                  results.add(getResultObj(node));
                }
              }
            } else { // search on question level
              if (mergeQuestion.isEmpty())
                return results;
              for (Node node : listAnswerandComment.values()) {
                String id = node.getProperty(EXO_QUESTION_ID).getString();
                if (mergeQuestion.containsKey(id)) {
                  results.add(getResultObj(node));
                }
              }
            }
          } else {
            for (Node node : listAnswerandComment.values()) {
              results.add(getResultObj(node));
            }
          }
        }
        // mix all result for fultext search on questions
        if (!eventQuery.isQuestionLevelSearch() && !eventQuery.isAnswerCommentLevelSearch() && !eventQuery.isLanguageLevelSearch()) {
          Map<String, ObjectSearchResult> tmpResult = new HashMap<String, ObjectSearchResult>();
          ObjectSearchResult rs;
          for (Node node : listAnswerandComment.values()) {
            rs = getResultObj(node);
            tmpResult.put(rs.getId(), rs);
          }
          for (Node node : listQuestion) {
            rs = getResultObj(node);
            tmpResult.put(rs.getId(), rs);
          }
          for (Node node : listLanguage) {
            rs = getResultObj(node);
            tmpResult.put(rs.getId(), rs);
          }
          results.addAll(tmpResult.values());
        }
        return results;

      } else if (eventQuery.getType().equals(FAQEventQuery.CATEGORY_AND_QUESTION)) { // Quick search
        String nodePath = EMPTY_STR;
        Session session = categoryHome.getSession();
        Map<String, ObjectSearchResult> searchMap = new HashMap<String, ObjectSearchResult>();

        while (iter.hasNext()) {
          boolean isResult = true;
          nodeObj = iter.nextNode();
          nodePath = nodeObj.getPath();
          if (nodePath.indexOf("/Question") > 0 && nodePath.lastIndexOf("/") >= nodePath.indexOf("/Question")) {
            nodePath = nodePath.substring(0, nodePath.indexOf("/Question") + 41);
            nodeObj = (Node) session.getItem(nodePath);
            if (!eventQuery.isAdmin()) {
              try {
                if ((nodeObj.getProperty(EXO_IS_APPROVED).getBoolean() == true && nodeObj.getProperty(EXO_IS_ACTIVATED).getBoolean() == true) || (nodeObj.getProperty(EXO_AUTHOR).getString().equals(eventQuery.getUserId()) && nodeObj.getProperty(EXO_IS_ACTIVATED).getBoolean() == true)) {
                  // for retricted audiences

                  if (retrictedCategoryList.size() > 0) {
                    String path = nodeObj.getPath();
                    for (String id : retrictedCategoryList) {
                      if (path.indexOf(id) > 0) {
                        isResult = false;
                        break;
                      }
                    }
                  }
                } else {
                  isResult = false;
                }
              } catch (Exception e) {
                log.debug(nodeObj + " node must exist: ", e);
                isResult = false;
              }
            }
          } else if (nodeObj.isNodeType(EXO_FAQ_CATEGORY)) {
            if (!eventQuery.isAdmin()) {
              // for restricted audiences
              if (retrictedCategoryList.size() > 0) {
                String path = nodeObj.getPath();
                for (String id : retrictedCategoryList) {
                  if (path.indexOf(id) > 0) {
                    isResult = false;
                    break;
                  }
                }
              }
            }
          }
          if (!searchMap.containsKey(nodeObj.getName()) && isResult) {
            searchMap.put(nodeObj.getName(), getResultObj(nodeObj));
          }
        }
        return new ArrayList<ObjectSearchResult>(searchMap.values());
      }
    } catch (Exception e) {
      throw e;
    }
    return new ArrayList<ObjectSearchResult>();
  }
  
  
  public List<ObjectSearchResult> getUnifiedSearchResults(FAQEventQuery eventQuery) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    eventQuery.setViewingCategories(getViewableCategoryIds(sProvider));
    
    //
    boolean isAdmin = isAdminRole(eventQuery.getUserId());
    
    //
    List<String> retrictedCategoryList = new ArrayList<String>();
    if (isAdmin == false) {
      retrictedCategoryList = getRetrictedCategories(eventQuery.getUserId(), eventQuery.getUserMembers());
    }

    //
    Node categoryHome = getCategoryHome(sProvider, null);
    eventQuery.setPath(categoryHome.getPath());
    
    //get origin text query
    String textQuery = CommonUtils.removeSpecialCharacterForSearch(eventQuery.getQuestion());
    
    try {
      
      QueryManager qm = categoryHome.getSession().getWorkspace().getQueryManager();
      Query query = qm.createQuery(eventQuery.getQuery(), Query.XPATH);
      QueryResult result = query.execute();
      
      //
      NodeIterator iter = result.getNodes();
      RowIterator rowIter = result.getRows();
      
      
      AnswerSearchResult searchResult = new AnswerSearchResult(eventQuery.getOffset(), eventQuery.getLimit(), iter.getSize());
      
      
      Node nodeObj = null;
      Row rowObj = null;
      
      ObjectSearchResult objectResult = null;

      while (iter.hasNext() && rowIter.hasNext()) {
        nodeObj = iter.nextNode();
        rowObj = rowIter.nextRow();
        
        //
        if (searchResult.contains(nodeObj.getName())) {
          continue;
        }

        String excerptField = "";
        if (nodeObj.isNodeType(EXO_FAQ_QUESTION) || nodeObj.isNodeType(EXO_FAQ_LANGUAGE)) {
          objectResult = ResultType.QUESTION.get(nodeObj, eventQuery, retrictedCategoryList);
          excerptField = EXO_NAME;
        } else if (nodeObj.isNodeType(EXO_ANSWER)) {
          objectResult = ResultType.ANSWER.get(nodeObj, eventQuery, retrictedCategoryList);
          excerptField = EXO_RESPONSES;
        } else if (nodeObj.isNodeType(EXO_COMMENT)) {
          objectResult = ResultType.COMMENT.get(nodeObj, eventQuery, retrictedCategoryList);
          excerptField = EXO_COMMENTS;
        }

        //
        if (objectResult == null) continue;

        //
        if (rowObj != null) {
          objectResult.setRelevancy(rowObj.getValue(JCR_SCORE).getLong());
          String excerpt = rowObj.getValue(String.format(REP_EXCERPT_PATTERN, excerptField)).getString();
          
          //check whether the excerpt have the highlight text
          if(!highlightPattern.matcher(excerpt).find() && excerptField.equals(EXO_NAME) && excerpt.toLowerCase().indexOf(textQuery) < 0){
            excerpt = rowObj.getValue(String.format(REP_EXCERPT_PATTERN, EXO_TITLE)).getString();
          }
          objectResult.setExcerpt(CommonUtils.getExcerpt(excerpt, textQuery, EXCERPT_MAX_LENGTH));
        }

        searchResult.add(objectResult);

        //
        if (searchResult.addMore() == false) {
          break;
        }
      }
     
      return searchResult.result();
      
    } catch (Exception e) {
      return new ArrayList<ObjectSearchResult>();
    }
  }
  
  private enum ResultType {

    QUESTION() {

      @Override
      public ObjectSearchResult get(Node node, FAQEventQuery eventQuery, List<String> retrictedCategoryList) throws Exception {
        if (node.isNodeType(EXO_FAQ_LANGUAGE)) {
          node = node.getParent().getParent();
        }
        if(checkQuestionHasApproved(node, eventQuery, retrictedCategoryList) == true) {
          return getQuestionObjectSearchResult(node);
        }
        return null;
      }
      
    },
    ANSWER() {

      @Override
      public ObjectSearchResult get(Node node, FAQEventQuery eventQuery, List<String> retrictedCategoryList) throws Exception {
        Node questionNode = getQuestionNode(node);
        ObjectSearchResult objectResult = ResultType.QUESTION.get(questionNode, eventQuery, retrictedCategoryList);
        if(objectResult == null) return null;
        objectResult.setId(node.getName());
        objectResult.setType("faqAnswer");
        PropertyReader reader = new PropertyReader(node);
        objectResult.setDescription(reader.string(EXO_RESPONSES));
        objectResult.setCreatedDate(reader.date(EXO_DATE_RESPONSE));
        return objectResult;
      }
      
    },
    COMMENT() {

      @Override
      public ObjectSearchResult get(Node node, FAQEventQuery eventQuery, List<String> retrictedCategoryList) throws Exception {
        Node questionNode = getQuestionNode(node);
        ObjectSearchResult objectResult = ResultType.QUESTION.get(questionNode, eventQuery, retrictedCategoryList);
        if(objectResult == null) return null;
        objectResult.setId(node.getName());
        objectResult.setType("faqComment");
        PropertyReader reader = new PropertyReader(node);
        objectResult.setDescription(reader.string(EXO_COMMENTS));
        objectResult.setCreatedDate(reader.date(EXO_DATE_COMMENT));
        return objectResult;
      }
      
    };
    
    /**
     * Get the data from the node and 
     * put this one into ObjectSearchResult.
     * 
     * @param node
     * @param eventQuery
     * @param retrictedCategoryList
     * @return
     * @throws Exception
     */
    protected abstract ObjectSearchResult get(Node node, FAQEventQuery eventQuery, List<String> retrictedCategoryList) throws Exception;
  }
  
  private ObjectSearchResult getResultObj(Node node) throws Exception {
    ObjectSearchResult objectResult = new ObjectSearchResult();
    if (node.isNodeType(EXO_FAQ_CATEGORY)) {
      objectResult.setIcon("FAQCategorySearch");
      objectResult.setName(node.getProperty(EXO_NAME).getString());
      objectResult.setType("faqCategory");
      String path = node.getPath();
      objectResult.setPath(path.substring(path.indexOf(Utils.FAQ_APP) + Utils.FAQ_APP.length() + 1));
      objectResult.setId(node.getName());
      objectResult.setCreatedDate(node.getProperty(EXO_CREATED_DATE).getDate().getTime());
    } else {
      if (node.isNodeType(EXO_FAQ_QUESTION)) {
        objectResult = getQuestionObjectSearchResult(node);
      } else {
        Node questionNode = getQuestionNode(node);
        objectResult = getQuestionObjectSearchResult(questionNode);
      }
    }

    return objectResult;
  }
  
  private static boolean checkQuestionHasApproved(Node questionNode, FAQEventQuery eventQuery,List<String>  retrictedCategoryList) throws Exception {
    if (questionNode != null && ((questionNode.getProperty(EXO_IS_APPROVED).getBoolean() == true && questionNode.getProperty(EXO_IS_ACTIVATED).getBoolean() == true)
        || (questionNode.getProperty(EXO_AUTHOR).getString().equals(eventQuery.getUserId()) && questionNode.getProperty(EXO_IS_ACTIVATED).getBoolean() == true)))
      // for retricted audiences
      if (retrictedCategoryList.size() > 0) {
        String path = questionNode.getPath();
        boolean isCanView = true;
        for (String id : retrictedCategoryList) {
          if (path.indexOf(id) > 0) {
            isCanView = false;
            break;
          }
        }
        if (isCanView)
          return true;
      } else {
        return true;
      }
    return false;
  }

  private static Node getQuestionNode(Node node) throws RepositoryException {
    if(node.isNodeType(EXO_FAQ_QUESTION)) {
      return node;
    } else {
      String nodePath = node.getPath();
      int i = nodePath.indexOf(Question.QUESTION_ID);
      if (i > 0) {
        nodePath = nodePath.substring(0, nodePath.indexOf("/", i));
        return (Node) node.getSession().getItem(nodePath);
      }
    }
    return null;
  }

  private static ObjectSearchResult getQuestionObjectSearchResult(Node questionNode) throws Exception {
    if(questionNode == null) return null;
    ObjectSearchResult objectResult = new ObjectSearchResult();
    objectResult.setType("faqQuestion");
    if (questionHasAnswer(questionNode)) {
      objectResult.setIcon("QuestionSearch");
    } else {
      objectResult.setIcon("NotResponseSearch");
    }
    String path = questionNode.getPath();

    PropertyReader reader = new PropertyReader(questionNode);
    objectResult.setName(reader.string(EXO_TITLE));
    objectResult.setDescription(reader.string(EXO_NAME));
    objectResult.setId(questionNode.getName());
    objectResult.setPath(path.substring(path.indexOf(Utils.FAQ_APP) + Utils.FAQ_APP.length() + 1));
    objectResult.setCreatedDate(reader.date(EXO_CREATED_DATE));
    objectResult.setNumberOfAnswer((int)reader.l(EXO_NUMBER_OF_PUBLIC_ANSWERS));
    objectResult.setNumberOfComment(getCommentSize(questionNode));
    objectResult.setRatingOfQuestion(reader.d(EXO_MARK_VOTE));
    objectResult.setLink(reader.string(EXO_LINK));
    return objectResult;
  }

  @Override
  public List<String> getCategoryPath(String categoryId) throws Exception {
    List<String> breadcums = new ArrayList<String>();
    try {
      Node category = getCategoryNodeById(categoryId);
      while (!category.getName().equals(Utils.CATEGORY_HOME)) {
        breadcums.add(category.getName());
        category = category.getParent();
      }
    } catch (Exception e) {
      log.error("Failed to get category: ", e);
    }
    return breadcums;
  }

  @Override
  public String getParentCategoriesName(String path) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    StringBuilder names = new StringBuilder();
    List<String> list = new ArrayList<String>();
    try {
      Node category = getFAQServiceHome(sProvider).getNode(path);
      while (category.isNodeType(EXO_FAQ_CATEGORY)) {
        if (category.getName().equals(Utils.CATEGORY_HOME) || category.hasProperty(EXO_NAME) == false) {
          list.add(category.getName());
        } else {
          list.add(category.getProperty(EXO_NAME).getString());
        }
        category = category.getParent();
      }
      Collections.reverse(list);
      Iterator<String> iter = list.iterator();
      boolean hasNext = iter.hasNext();
      while (hasNext) {
        names.append(iter.next());
        hasNext = iter.hasNext();
        if (hasNext) {
          names.append(" > ");
        }
      }
    } catch (Exception e) {
      log.error("Failed to get parent categories name", e);
    }
    return names.toString();
  }

  private void sendEmailNotification(List<String> addresses, Message message) {
    pendingMessagesQueue.add(new NotifyInfo(addresses, message));
  }

  @Override
  public Iterator<NotifyInfo> getPendingMessages() {
    Iterator<NotifyInfo> pending = new ArrayList<NotifyInfo>(pendingMessagesQueue).iterator();
    pendingMessagesQueue.clear();
    return pending;
  }

  @Override
  public NotifyInfo getMessageInfo(String name) throws Exception {
    NotifyInfo messageInfo = messagesInfoMap_.get(name);
    messagesInfoMap_.remove(name);
    return messageInfo;
  }

  @Override
  public void swapCategories(String cateId1, String cateId2) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      String[] strs = cateId2.split(",");
      boolean isTop = (strs.length > 1 && strs[1].trim().length() > 0);
      Node faqHome = getFAQServiceHome(sProvider);
      Node goingCategory = faqHome.getNode(cateId1);
      Node mockCategory = faqHome.getNode(strs[0].trim());
      long index = (isTop) ? 0 : mockCategory.getProperty(EXO_INDEX).getLong();
      if (goingCategory.getParent().getPath().equals(mockCategory.getParent().getPath())) {
        long gindex = goingCategory.getProperty(EXO_INDEX).getLong();
        resetIndex(goingCategory, index, gindex);
      } else {
        Node parent = mockCategory.getParent();
        String asPath = parent.getPath().replaceFirst(dataLocator.getFaqHomeLocation(), EMPTY_STR).replaceFirst("//", EMPTY_STR);
        if (!isCategoryExist(sProvider, new PropertyReader(goingCategory).string(EXO_NAME, EMPTY_STR), asPath)) {
          Node parentNode = goingCategory.getParent();
          String id = goingCategory.getName();
          mockCategory.getSession().move(goingCategory.getPath(), parent.getPath() + "/" + id);
          faqHome.getSession().save();
          Node destCat = parent.getNode(id);
          long l = 1;
          if (!isTop) {
            l = parent.getNodes().getSize();
            destCat.setProperty(EXO_INDEX, l);
            parent.save();
          }
          resetIndex(destCat, index, l);
          // update index for children categories of parent category moved.
          reUpdateIndex(parentNode);
        } else {
          throw new RuntimeException();
        }
      }
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      log.debug("Failed to swap categories.", e);
    }
  }

  private NodeIterator getCategoriesIterator(Node parentCategory) throws Exception {
    QueryManager qm = parentCategory.getSession().getWorkspace().getQueryManager();
    StringBuffer queryString = new StringBuffer(JCR_ROOT).append(parentCategory.getPath());
    queryString.append("/element(*,").append(EXO_FAQ_CATEGORY).append(") order by @").append(EXO_INDEX).append(" ascending");
    Query query = qm.createQuery(queryString.toString(), Query.XPATH);
    QueryResult result = query.execute();
    return result.getNodes();
  }

  @Override
  public void saveTopicIdDiscussQuestion(String questionId, String topicId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node questionNode = getFAQServiceHome(sProvider).getNode(questionId);
      questionNode.setProperty(EXO_TOPIC_ID_DISCUSS, topicId);
      questionNode.save();
    } catch (Exception e) {
      log.error("Failed to save topic discuss question: ", e);
    }
  }

  @Override
  public InputStream exportData(String categoryId, boolean createZipFile) throws Exception {
    Node categoryNode = getCategoryNodeById(categoryId);
    Session session = categoryNode.getSession();
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    File file = null;
    List<File> listFiles = new ArrayList<File>();
    session.exportSystemView(categoryNode.getPath(), bos, false, false);
    listFiles.add(CommonUtils.getXMLFile(bos, "eXo Knowledge Suite - Answers", "Category", CommonUtils.getGreenwichMeanTime().getTime(), categoryNode.getName()));
    ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream("exportCategory.zip"));
    try {
      int byteReads;
      byte[] buffer = new byte[4096]; // Create a buffer for copying
      FileInputStream inputStream = null;
      ZipEntry zipEntry = null;
      for (File f : listFiles) {
        try {
          inputStream = new FileInputStream(f);
          zipEntry = new ZipEntry(f.getPath());
          zipOutputStream.putNextEntry(zipEntry);
          while ((byteReads = inputStream.read(buffer)) != -1)
            zipOutputStream.write(buffer, 0, byteReads);
        } finally {
          inputStream.close();
        }
      }
    } finally {
      zipOutputStream.close();
    }

    file = new File("exportCategory.zip");
    InputStream fileInputStream = new FileInputStream(file);
    return fileInputStream;
  }

  @Override
  public boolean importData(String parentId, InputStream inputStream, boolean isZip) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
      List<String> patchNodeImport = new ArrayList<String>();
      Node categoryNode = getFAQServiceHome(sProvider).getNode(parentId);
      Session session = categoryNode.getSession();
      NodeIterator iter = categoryNode.getNodes();
      while (iter.hasNext()) {
        patchNodeImport.add(iter.nextNode().getName());
      }
      if (isZip) { // Import from zipfile
        ZipInputStream zipStream = new ZipInputStream(inputStream);
        while (zipStream.getNextEntry() != null) {
          ByteArrayOutputStream out = new ByteArrayOutputStream();
          int available = -1;
          byte[] data = new byte[2048];
          while ((available = zipStream.read(data, 0, 1024)) > -1) {
            out.write(data, 0, available);
          }
          zipStream.closeEntry();
          out.close();
          InputStream input = new ByteArrayInputStream(out.toByteArray());
          session.importXML(categoryNode.getPath(), input, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW);
          session.save();
        }
        zipStream.close();
        calculateImportRootCategory(categoryNode);
      } else { // import from xml
        session.importXML(categoryNode.getPath(), inputStream, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW);
        session.save();
      }
      categoryNode = (Node) session.getItem(categoryNode.getPath());
      iter = categoryNode.getNodes();
      while (iter.hasNext()) {
        Node node = iter.nextNode();
        if (patchNodeImport.contains(node.getName()))
          patchNodeImport.remove(node.getName());
        else
          patchNodeImport.add(node.getName());
      }
      for (String string : patchNodeImport) {
        Node nodeParentQuestion = categoryNode.getNode(string);
        iter = getQuestionsIterator(nodeParentQuestion, EMPTY_STR, true);
        // Update number answers and regeister question node listener
        while (iter.hasNext()) {
          Node node = iter.nextNode();
          reUpdateNumberOfPublicAnswers(node);
        }
      }
    return true;
  }

  private void calculateImportRootCategory(Node categoryRootNode) {
    try {
      if(categoryRootNode.hasNode(Utils.CATEGORY_HOME)) {
        Node categoryNode = categoryRootNode.getNode(Utils.CATEGORY_HOME);
        NodeIterator iterator = categoryRootNode.getNodes();
        int i = 0;
        while (iterator.hasNext()) {
          if (iterator.nextNode().isNodeType(EXO_FAQ_CATEGORY)) {
            i = i + 1;
          }
        }
        String rootPath = categoryRootNode.getPath();
        Session session = categoryRootNode.getSession();
        Workspace workspace = session.getWorkspace();
        iterator = categoryNode.getNodes();
        while (iterator.hasNext()) {
          Node node = iterator.nextNode();
          try {
            if (node.isNodeType(EXO_FAQ_CATEGORY)) {
              node.setProperty(EXO_INDEX, i);
              i = i + 1;
              workspace.move(node.getPath(), rootPath + "/" + node.getName());
            } else if (node.isNodeType(EXO_FAQ_QUESTION_HOME)) {
              if (categoryRootNode.hasNode(Utils.QUESTION_HOME)) {
                NodeIterator iter = node.getNodes();
                while (iter.hasNext()) {
                  Node node_ = iter.nextNode();
                  workspace.move(node_.getPath(), rootPath + "/" + Utils.QUESTION_HOME + "/" + node_.getName());
                }
              } else {
                workspace.move(node.getPath(), rootPath + "/" + node.getName());
              }
            }
          } catch (Exception e) {
            if (log.isDebugEnabled()) {
              log.debug("Failed to move node " + node.getName(), e);
            }
          }
        }
        categoryNode.remove();
        session.save();
      }
    } catch (Exception e) {
        log.warn("Failed to calculate imported root category");
    }
  }

  @Override
  public boolean isExisting(String path) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    return getFAQServiceHome(sProvider).hasNode(path);
  }

  @Override
  public String getCategoryPathOf(String id) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node node = getFAQServiceHome(sProvider).getNode(id);
      String path = null;
      if (node != null) {
        if (node.isNodeType(EXO_FAQ_QUESTION)) {
          path = node.getParent().getParent().getPath();
        } else if (node.isNodeType(EXO_FAQ_CATEGORY)) {
          path = node.getPath();
        }
        return path.substring(path.indexOf(Utils.CATEGORY_HOME));
      }
    } catch (Exception e) {
      logDebug("Failed to get category of path: " + id + "\n" + e.getMessage());
    }
    return null;
  }
  
  private boolean getBooleanPropertyOfCategory(String id, String property) {
    try {
      SessionProvider sProvider = CommonUtils.createSystemProvider();
      Node node = getQuestionNode(sProvider, id);
      if (node != null && node.isNodeType(EXO_FAQ_QUESTION)) {
        node = node.getParent().getParent();
      } else {
        node = getCategoryNode(sProvider, id);
      }
      return new PropertyReader(node).bool(property);
    } catch (Exception e) {
      return false;
    }
  }

  @Override
  public boolean isModerateAnswer(String id) {
    return getBooleanPropertyOfCategory(id, EXO_IS_MODERATE_ANSWERS);
  }

  @Override
  public boolean isModerateQuestion(String id) {
    return getBooleanPropertyOfCategory(id, EXO_IS_MODERATE_QUESTIONS);
  }

  @Override
  public boolean isViewAuthorInfo(String id) {
    return getBooleanPropertyOfCategory(id, EXO_VIEW_AUTHOR_INFOR);
  }

  @Override
  public boolean isCategoryModerator(String categoryId, String user) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node node = getCategoryNode(sProvider, categoryId);
      List<String> values = new PropertyReader(node).list(EXO_MODERATORS, new ArrayList<String>());
      return Utils.hasPermission(values, UserHelper.getAllGroupAndMembershipOfUser(user));
    } catch (Exception e) {
      return false;
    }
  }

  @Override
  public boolean isCategoryExist(String name, String path) {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    return isCategoryExist(sProvider, name, path);
  }

  private boolean isCategoryExist(SessionProvider sProvider, String name, String path) {
    if (CommonUtils.isEmpty(path)) {
      path = Utils.CATEGORY_HOME;
    }
    try {
      NodeIterator iter = getCategoryNode(sProvider, path).getNodes();
      while (iter.hasNext()) {
        Node catNode = iter.nextNode();
        if (catNode.isNodeType(EXO_FAQ_CATEGORY)) {
          if (new PropertyReader(catNode).string(EXO_NAME, EMPTY_STR).equalsIgnoreCase(name)) {
            return true;
          }
        }
      }
    } catch (Exception e) {
      return false;
    }
    return false;
  }

  @Override
  public List<String> getQuestionContents(List<String> paths) throws Exception {
    PropertyReader reader = null;
    List<String> contents = new ArrayList<String>();
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    for (String path : paths) {
      try {
        reader = new PropertyReader(getQuestionNode(sProvider, path));
        contents.add(reader.string(EXO_TITLE));
      } catch (Exception e) {
        if (log.isDebugEnabled()) {
          log.debug("Failed to get question node with path " + path, e);
        }
      }
    }
    return contents;
  }
  
  /**
   * Get a map which contains id as questionID and value as question's title
   * @param paths
   * @return a map which has key: questionID and value: question title
   * @throws Exception
   */
  public Map<String, String> getRelationQuestion(List<String> paths) throws Exception {
    Map<String, String> mReturn = new LinkedHashMap<String, String>();
    PropertyReader reader = null;
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    for (String path : paths) {
      try {
        reader = new PropertyReader(getQuestionNode(sProvider, path));
        if(reader.bool(EXO_IS_ACTIVATED) && reader.bool(EXO_IS_APPROVED)){
          mReturn.put(path, reader.string(EXO_TITLE));
        }
      } catch (Exception e) {
        log.error("getRelationQuestion fails, exception:", e);
      }
    }
    return mReturn;
  }
  

  // will be remove
  @Override
  public Node getQuestionNodeById(String path) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      return getQuestionNode(sProvider, path);
    } catch (Exception e) {
      log.error("Failed to get question node by path:" + path, e);
    }
    return null;
  }

  /**
   *Get Node question by param
   *
   *@param: sProvider the @SessionProvider
   *@param: param the question id or question relative path.
   *@return: the question Node.
  */
  private Node getQuestionNode(SessionProvider sProvider, String param) throws Exception {
    Node serviceHome = getFAQServiceHome(sProvider);
    try {
      return serviceHome.getNode(param);
    } catch (PathNotFoundException e) {
      param = (param.indexOf("/") > 0) ? param.substring(param.lastIndexOf("/") + 1) : param;
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(serviceHome.getPath()).append("//element(*,exo:faqQuestion)[fn:name()='").append(param).append("']");
      QueryManager qm = serviceHome.getSession().getWorkspace().getQueryManager();
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator iter = result.getNodes();
      if (iter.getSize() > 0)
        return iter.nextNode();
    } catch (Exception e) {
      // The function return null when can not get Question Node by question id.
      return null;
    }
    return null;
  }

  @Override
  public String[] getModeratorsOf(String path) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node node = getFAQServiceHome(sProvider).getNode(path);
      if (node.isNodeType(EXO_FAQ_QUESTION)) {
        return new PropertyReader(node.getParent().getParent()).strings(EXO_MODERATORS, new String[] {});
      } else if (node.isNodeType(EXO_FAQ_CATEGORY)) {
        return new PropertyReader(node).strings(EXO_MODERATORS, new String[] {});
      }
    } catch (Exception e) {
      log.error("Failed to get moderators of path: " + path, e);
    }
    return new String[] {};
  }

  @Override
  public String getCategoryNameOf(String categoryPath) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node node = getFAQServiceHome(sProvider).getNode(categoryPath);
      return new PropertyReader(node).string(EXO_NAME, node.getName());
    } catch (Exception e) {
      log.error("Failed to get category name of path: " + categoryPath, e);
    }
    return null;
  }

  @Override
  public CategoryInfo getCategoryInfo(String categoryPath, List<String> categoryIdScoped) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    CategoryInfo categoryInfo = new CategoryInfo();
    try {
      Node categoryNode = getFAQServiceHome(sProvider).getNode(categoryPath);
      categoryInfo.setId(categoryNode.getName());
      String path = categoryNode.getPath();
      categoryInfo.setPath(path.substring(path.indexOf(Utils.FAQ_APP) + Utils.FAQ_APP.length() + 1));
      if (categoryNode.hasProperty(EXO_NAME))
        categoryInfo.setName(categoryNode.getProperty(EXO_NAME).getString());
      else
        categoryInfo.setName(categoryNode.getName());
      // set Path Name
      Node node = categoryNode;
      List<String> pathName = new ArrayList<String>();
      String categoryName;
      while (node.isNodeType(EXO_FAQ_CATEGORY)) {
        if (node.hasProperty(EXO_NAME)) {
          categoryName = node.getProperty(EXO_NAME).getString();
        } else {
          categoryName = node.getName();
        }
        pathName.add(categoryName);
        node = node.getParent();
      }
      categoryInfo.setPathName(pathName);
      // declare question info
      categoryInfo.setQuestionInfos(getQuestionInfo(categoryNode));

      // declare category info
      if (categoryNode.hasNodes()) {
        List<SubCategoryInfo> subList = new ArrayList<SubCategoryInfo>();
        
        List<String> listOfUser = UserHelper.getAllGroupAndMembershipOfUser(null);
        StringBuilder strQuery = new StringBuilder();
        
        strQuery.append(JCR_ROOT).append(categoryNode.getPath()).append("/element(*,").append(EXO_FAQ_CATEGORY).append(")[")
        
        .append("(").append(Utils.buildXpathHasProperty(EXO_USER_PRIVATE))
        .append(" or ").append(Utils.buildQueryListOfUser(EXO_USER_PRIVATE, listOfUser)).append(")")
        .append(" or (").append(Utils.buildQueryListOfUser(EXO_MODERATORS, listOfUser)).append(")")
        .append("]");
        
        QueryManager qm = categoryNode.getSession().getWorkspace().getQueryManager();
        Query query = qm.createQuery(strQuery.toString(), Query.XPATH);
        QueryResult result = query.execute();
        
        NodeIterator subIter = result.getNodes();
        Node sub;
        SubCategoryInfo subCat;
        while (subIter.hasNext()) {
          sub = subIter.nextNode();
          if (categoryIdScoped.isEmpty() || categoryIdScoped.contains(sub.getName())) {
            if (sub.isNodeType(EXO_FAQ_CATEGORY)) {
              subCat = new SubCategoryInfo();
              subCat.setId(sub.getName());
              subCat.setName(sub.getProperty(EXO_NAME).getString());
              subCat.setPath(categoryInfo.getPath() + "/" + sub.getName());
              subCat.setSubCateInfos(getSubCategoryInfo(sub, categoryIdScoped));
              subCat.setQuestionInfos(getQuestionInfo(sub));
              subList.add(subCat);
            }
          }
        }
        categoryInfo.setSubCateInfos(subList);
      }
    } catch (Exception e) {
      return null;
    }
    return categoryInfo;
  }

  public void reCalculateInfoOfQuestion(String absPathOfProp) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Item item = null;
      Node quesNode = null;
      Node serviceHomeNode = getFAQServiceHome(sProvider);

      // ----- get Question Node -------------
      int quesNameIndex = absPathOfProp.lastIndexOf(Utils.QUESTION_HOME) + Utils.QUESTION_HOME.length() + 2;
      String quesPath = absPathOfProp.substring(0, absPathOfProp.indexOf("/", quesNameIndex));
      try {
        quesNode = (Node) serviceHomeNode.getSession().getItem(quesPath);
      } catch (PathNotFoundException pe) {
        return;
      }
      String lastActivityInfo = null;
      if (quesNode.hasProperty(EXO_LAST_ACTIVITY))
        lastActivityInfo = quesNode.getProperty(EXO_LAST_ACTIVITY).getString();
      long timeOfLastActivity = Utils.getTimeOfLastActivity(lastActivityInfo);
      long numberOfAnswers = 0;
      if (quesNode.hasProperty(EXO_NUMBER_OF_PUBLIC_ANSWERS)) {
        numberOfAnswers = quesNode.getProperty(EXO_NUMBER_OF_PUBLIC_ANSWERS).getLong();
      }
      // ------------- end -----------------

      // -------------- get updated Item ----------------
      try {
        item = getFAQServiceHome(sProvider).getSession().getItem(absPathOfProp);
      } catch (PathNotFoundException pnfe) {
        // item has been removed. Update last activity of question.
        reUpdateLastActivityOfQuestion(quesNode);
        reUpdateNumberOfPublicAnswers(quesNode);
        return;
      }

      if (item instanceof Property) {
        Property prop = (Property) item;
        if (prop.getName().equalsIgnoreCase(EXO_ACTIVATE_RESPONSES) || prop.getName().equalsIgnoreCase(EXO_APPROVE_RESPONSES)) {
          // if activate or approve property has been changed.
          Node answerNode = prop.getParent();
          boolean isActivated = false, isApproved = false;
          if (answerNode.hasProperty(EXO_ACTIVATE_RESPONSES))
            isActivated = answerNode.getProperty(EXO_ACTIVATE_RESPONSES).getBoolean();
          if (answerNode.hasProperty(EXO_APPROVE_RESPONSES))
            isApproved = answerNode.getProperty(EXO_APPROVE_RESPONSES).getBoolean();
          long answerTime = 0;
          if (answerNode.hasProperty(EXO_DATE_RESPONSE))
            answerTime = answerNode.getProperty(EXO_DATE_RESPONSE).getDate().getTimeInMillis();
          if (isActivated && isApproved) {
            numberOfAnswers++;
            quesNode.setProperty(EXO_NUMBER_OF_PUBLIC_ANSWERS, numberOfAnswers);
            // admin changed this answer to public ...
            if (timeOfLastActivity < answerTime) {
              String author = answerNode.getProperty(EXO_RESPONSE_BY).getString();
              quesNode.setProperty(EXO_LAST_ACTIVITY, getLastActivityInfo(author, answerTime));
            }
            quesNode.save();

            return;
          } else {
            // if admin change answer status from viewable to unapproved and
            // inactivated
            reUpdateNumberOfPublicAnswers(quesNode);
            // reUpdateLastActivityOfQuestion(quesNode);
            if (timeOfLastActivity == answerTime) {
              // re-update last activity now
              reUpdateLastActivityOfQuestion(quesNode);
              return;
            }
          }
        }

      }

      if (item instanceof Node) {
        // case of adding new comment.
        Node node = (Node) item;
        if (node.getPrimaryNodeType().getName().equalsIgnoreCase(EXO_COMMENT)) {
          long commentTime = node.getProperty(EXO_DATE_COMMENT).getDate().getTimeInMillis();
          if (commentTime > timeOfLastActivity) {
            String author = node.getProperty(EXO_COMMENT_BY).getString();
            quesNode.setProperty(EXO_LAST_ACTIVITY, getLastActivityInfo(author, commentTime));
            quesNode.save();
          }
        }
      }
    } catch (Exception e) {
      log.error("Failed to re calculateInfo of question", e);
    }
  }

  private void reUpdateNumberOfPublicAnswers(Node questionNode) throws RepositoryException {
    QueryManager qm = questionNode.getSession().getWorkspace().getQueryManager();
    StringBuilder sb = new StringBuilder();
    sb.append(JCR_ROOT).append(questionNode.getPath()).append("//element(*, exo:answer)[@exo:activateResponses='true' and @exo:approveResponses='true']");
    Query query = qm.createQuery(sb.toString(), Query.XPATH);
    QueryResult result = query.execute();
    long size = result.getNodes().getSize();
    size = size < 0 ? 0 : size;
    questionNode.setProperty(EXO_NUMBER_OF_PUBLIC_ANSWERS, size);
    questionNode.save();

  }

  private void reUpdateLastActivityOfQuestion(Node quesNode) throws RepositoryException {
    QueryManager qm = quesNode.getSession().getWorkspace().getQueryManager();

    StringBuilder sb = new StringBuilder();
    sb.append(JCR_ROOT).append(quesNode.getPath()).append("//element(*, exo:answer)[@exo:activateResponses='true' and @exo:approveResponses='true'] order by @exo:dateResponse descending");
    QueryImpl query = (QueryImpl) qm.createQuery(sb.toString(), Query.XPATH);
    query.setLimit(1);
    QueryResult result = query.execute();
    NodeIterator iter = result.getNodes();

    String author = null;
    long lastTime = -1;
    if (iter.hasNext()) {
      Node node = iter.nextNode();

      if (node.hasProperty(EXO_DATE_RESPONSE)) {
        lastTime = node.getProperty(EXO_DATE_RESPONSE).getDate().getTimeInMillis();
      }
      if (node.hasProperty(EXO_RESPONSE_BY)) {
        author = node.getProperty(EXO_RESPONSE_BY).getString();
      }

    }

    sb = new StringBuilder();
    sb.append(JCR_ROOT).append(quesNode.getPath()).append("//element(*, exo:comment) order by @exo:dateComment descending");
    query = (QueryImpl) qm.createQuery(sb.toString(), Query.XPATH);
    query.setLimit(1);
    result = query.execute();
    iter = result.getNodes();
    if (iter.hasNext()) {
      Node commentNode = iter.nextNode();
      if (commentNode.hasProperty(EXO_DATE_COMMENT) && commentNode.hasProperty(EXO_COMMENT_BY)) {
        long commentTime = commentNode.getProperty(EXO_DATE_COMMENT).getDate().getTimeInMillis();
        if (lastTime < commentTime) {
          lastTime = commentTime;
          author = commentNode.getProperty(EXO_COMMENT_BY).getString();
        }
      }
    }
    if (lastTime > 0) {
      quesNode.setProperty(EXO_LAST_ACTIVITY, getLastActivityInfo(author, lastTime));
    } else {
      quesNode.setProperty(EXO_LAST_ACTIVITY, EMPTY_STR);
    }
    quesNode.save();
  }

  private List<SubCategoryInfo> getSubCategoryInfo(Node category, List<String> categoryIdScoped) throws Exception {
    List<SubCategoryInfo> subList = new ArrayList<SubCategoryInfo>();
    if (category.hasNodes()) {
      NodeIterator iter = category.getNodes();
      Node sub;
      SubCategoryInfo cat;
      while (iter.hasNext()) {
        try {
          sub = iter.nextNode();
          if (sub.isNodeType(EXO_FAQ_CATEGORY)) {
            if (categoryIdScoped.isEmpty() || categoryIdScoped.contains(sub.getName())) {
              cat = new SubCategoryInfo();
              cat.setName(sub.getProperty(EXO_NAME).getString());
              String path = sub.getPath();
              cat.setPath(path.substring(path.indexOf(Utils.FAQ_APP) + Utils.FAQ_APP.length() + 1));
              cat.setId(sub.getName());
              subList.add(cat);
            }
          }
        } catch (Exception e) {
          log.error("Failed to get sub category info: ", e);
        }
      }
    }
    return subList;
  }

  private NodeIterator getNodeIteratorAnswerAccess(Node answerHome) throws Exception {
    StringBuffer queryString = new StringBuffer(JCR_ROOT).append(answerHome.getPath()).append("/element(*,exo:answer)[@exo:approveResponses='true' and @exo:activateResponses='true']");
    QueryManager qm = answerHome.getSession().getWorkspace().getQueryManager();
    Query query = qm.createQuery(queryString.toString(), Query.XPATH);
    QueryResult result = query.execute();
    return result.getNodes();
  }

  private List<QuestionInfo> getQuestionInfo(Node categoryNode) throws Exception {
    List<QuestionInfo> questionInfoList = new ArrayList<QuestionInfo>();
    if (categoryNode.hasNode(Utils.QUESTION_HOME)) {
      QuestionInfo questionInfo;
      String strQuery = "[@exo:isActivated='true' and @exo:isApproved='true']";
      NodeIterator iter = getQuestionsIterator(categoryNode.getNode(Utils.QUESTION_HOME), strQuery, false);
      Node question;
      while (iter.hasNext()) {
        question = iter.nextNode();
        questionInfo = new QuestionInfo();
        try {
          questionInfo.setQuestion(question.getProperty(EXO_TITLE).getString());
          questionInfo.setDetail(question.getProperty(EXO_NAME).getString());
          questionInfo.setId(question.getName());
          if (question.hasNode(Utils.ANSWER_HOME)) {
            List<Answer> answers = new ArrayList<Answer>();
            NodeIterator ansIter = getNodeIteratorAnswerAccess(question.getNode(Utils.ANSWER_HOME));
            Answer answer;
            Node node;
            PropertyReader reader;
            while (ansIter.hasNext()) {
              node = ansIter.nextNode();
              reader = new PropertyReader(node);
              answer = new Answer();
              answer.setId(node.getName());
              answer.setDateResponse(reader.date(EXO_DATE_RESPONSE));
              answer.setResponseBy(reader.string(EXO_RESPONSE_BY));
              answer.setResponses(reader.string(EXO_RESPONSES));
              answer.setMarkVotes(reader.l(EXO_MARK_VOTES));
              answers.add(answer);
            }
            questionInfo.setAnswers(answers);
          }
          questionInfoList.add(questionInfo);
        } catch (Exception e) {
          log.error("Failed to add answer by question node: " + question.getName(), e);
        }
      }
    }
    return questionInfoList;
  }

  @Override
  public void updateQuestionRelatives(String questionPath, String[] relatives) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node question = getFAQServiceHome(sProvider).getNode(questionPath);
      question.setProperty(EXO_RELATIVES, relatives);
      question.save();
    } catch (Exception e) {
      log.error("Failed to update question relatives: ", e);
    }
  }

  @Override
  public void calculateDeletedUser(String userName) throws Exception {
    // remove setting by user
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node node = getUserSettingHome(sProvider);
      if (node.hasNode(userName))
        node.getNode(userName).remove();
      StringBuilder queryString = new StringBuilder(JCR_ROOT).append("/").append(dataLocator.getFaqCategoriesLocation()).append("//*[");
      String[] strs = new String[] { EXO_RESPONSE_BY, EXO_COMMENT_BY, EXO_AUTHOR, EXO_USER_WATCHING };
      for (int i = 0; i < strs.length; i++) {
        queryString.append("@").append(strs[i]).append("='").append(userName).append((i == strs.length - 1) ? "']" : "' or ");
      }
      Session session = node.getSession();
      QueryManager qm = session.getWorkspace().getQueryManager();
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator iter = result.getNodes();
      Node item;
      String newUserName = userName + Utils.DELETED + (new Random().nextInt(1000));
      PropertyReader reader;
      while (iter.hasNext()) {
        item = iter.nextNode();
        reader = new PropertyReader(item);
        for (int i = 0; i < strs.length; i++) {
          if (i < 3 && reader.string(strs[i], "").equals(userName)) {
            item.setProperty(strs[i], newUserName);
          } else {
            List<String> list = reader.list(strs[i], new ArrayList<String>());
            if (list.size() > 0 && list.contains(userName)) {
              list.remove(userName);
              item.setProperty(strs[i], list.toArray(new String[list.size()]));
            }
          }
        }
      }
      session.save();
      // LastActivity
      queryString = new StringBuilder(JCR_ROOT).append("/").append(dataLocator.getFaqCategoriesLocation())
                                               .append("//element(*,").append(EXO_FAQ_QUESTION).append(")[(jcr:contains(@")
                                               .append(EXO_LAST_ACTIVITY).append(", '").append(userName).append(Utils.HYPHEN).append("*'))]");
      query = qm.createQuery(queryString.toString(), Query.XPATH);
      iter = query.execute().getNodes();
      Question question = new Question();
      String info;
      while (iter.hasNext()) {
        item = iter.nextNode();
        info = new PropertyReader(item).string(EXO_LAST_ACTIVITY);
        if (!CommonUtils.isEmpty(info)) {
          question.setLastActivity(info);
          if (userName.equals(question.getAuthorOfLastActivity())) {
            item.setProperty(EXO_LAST_ACTIVITY, getLastActivityInfo(newUserName, question.getTimeOfLastActivity()));
          }
        }
      }
      session.save();
    } catch (Exception e) {
      log.debug("Failed to calculate delete user: " + userName, e);
    }
  }

  public InputStream createAnswerRSS(String cateId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    try {
      Node cateNode = getCategoryNode(sProvider, cateId);
      List<SyndEntry> entries = new ArrayList<SyndEntry>();
      StringBuilder queryString = new StringBuilder(JCR_ROOT).append(cateNode.getPath()).append("//element(*,exo:faqQuestion)");
      List<String> list = getListCategoryIdPublic(sProvider, cateNode);
      if (!list.isEmpty())
        queryString.append("[");
      PropertyReader reader = new PropertyReader(cateNode);
      if (reader.list(EXO_USER_PRIVATE, new ArrayList<String>()).isEmpty()) {
        if (!list.isEmpty())
          list.add(cateNode.getName());
      }
      boolean isOr = false;
      for (String id : list) {
        if (isOr) {
          queryString.append(" or (@exo:categoryId='").append(id).append("')");
        } else {
          queryString.append("(@exo:categoryId='").append(id).append("')");
          isOr = true;
        }
      }
      if (!list.isEmpty())
        queryString.append("]");

      QueryManager qm = cateNode.getSession().getWorkspace().getQueryManager();
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator iter = result.getNodes();
      Node nodeQs;

      while (iter.hasNext()) {
        nodeQs = iter.nextNode();
        if (nodeQs.getParent().getParent().isNodeType(EXO_FAQ_CATEGORY)) {
          entries.add(createQuestionEntry(sProvider, nodeQs));
        }
      }

      SyndFeed feed = createNewFeed(cateNode, "http://www.exoplatform.com");
      feed.setEntries(entries);

      SyndFeedOutput output = new SyndFeedOutput();
      String s = output.outputString(feed);
      s = StringUtils.replace(s, "&amp;", "&");
      s = s.replaceAll("&lt;", "<").replaceAll("&gt;", ">");
      s = StringUtils.replace(s, "ST[CDATA[", "<![CDATA[");
      s = StringUtils.replace(s, "END]]", "]]>");

      return new ByteArrayInputStream(s.getBytes());
    } catch (Exception e) {
      log.error("Failed to create answer RSS ", e);
    }
    return null;
  }

  private List<String> getListCategoryIdPublic(SessionProvider sProvider, Node cateNode) throws Exception {
    List<String> list = new ArrayList<String>();

    StringBuilder queryString = new StringBuilder(JCR_ROOT).append(cateNode.getPath()).append("//element(*,exo:faqCategory)[@exo:isView='true' and ( not(@exo:userPrivate) or @exo:userPrivate='')]");

    QueryManager qm = cateNode.getSession().getWorkspace().getQueryManager();
    Query query = qm.createQuery(queryString.toString(), Query.XPATH);
    QueryResult result = query.execute();
    NodeIterator iter = result.getNodes();
    while (iter.hasNext()) {
      list.add(iter.nextNode().getName());
    }
    return list;
  }

  private SyndEntry createQuestionEntry(SessionProvider sProvider, Node questionNode) throws Exception {
    // Create new entry
    List<String> listContent = new ArrayList<String>();
    StringBuffer content = new StringBuffer();
    for (String answer : getStrAnswers(questionNode))
      content.append(answer);
    for (String comment : getComments(questionNode))
      content.append(comment);

    listContent.add(content.toString());
    SyndEntry entry = createNewEntry(questionNode, listContent);
    return entry;
  }

  private List<String> getStrAnswers(Node questionNode) throws Exception {
    List<String> listAnswers = new ArrayList<String>();
    try {
      Node answerHome = questionNode.getNode(Utils.ANSWER_HOME);
      QueryManager qm = answerHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(answerHome.getPath()).append("//element(*,exo:answer)[(@exo:approveResponses='true') and (@exo:activateResponses='true')]").append("order by @exo:dateResponse ascending");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator nodeIterator = result.getNodes();
      Node answerNode = null;
      while (nodeIterator.hasNext()) {
        answerNode = nodeIterator.nextNode();
        if (answerNode.hasProperty(EXO_RESPONSES))
          listAnswers.add("<strong><u>Answer:</u></strong> " + (answerNode.getProperty(EXO_RESPONSES).getString()) + "<br/>");
      }
    } catch (Exception e) {
      log.error("Failed to get answers for " + questionNode.getName());
    }
    return listAnswers;
  }

  public Comment[] getComments(String questionId) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    Node questionNode = getQuestionNode(sProvider, questionId);
    return getComment(questionNode);
  }

  private List<String> getComments(Node questionNode) throws Exception {
    List<String> listComment = new ArrayList<String>();
    try {
      Node commentHome = questionNode.getNode(Utils.COMMENT_HOME);
      QueryManager qm = commentHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer(JCR_ROOT).append(commentHome.getPath()).append("//element(*,exo:comment)").append(" order by @exo:dateComment ascending");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator nodeIterator = result.getNodes();
      Node commentNode = null;
      while (nodeIterator.hasNext()) {
        commentNode = nodeIterator.nextNode();
        if (commentNode.hasProperty(EXO_COMMENTS))
          listComment.add("<strong><u>Comment:</u></strong>" + (commentNode.getProperty(EXO_COMMENTS).getString()) + "<br/>");
      }
    } catch (Exception e) {
      log.error("Failed to get comments for " + questionNode.getName());
    }
    return listComment;
  }

  private SyndFeed createNewFeed(Node node, String link) throws Exception {
    PropertyReader reader = new PropertyReader(node);
    String desc = reader.string(EXO_DESCRIPTION, " ");
    SyndFeed feed = new SyndFeedImpl();
    feed.setFeedType("rss_2.0");
    feed.setTitle("ST[CDATA[" + reader.string(EXO_NAME, "Root") + "END]]");
    feed.setPublishedDate(reader.date(EXO_CREATED_DATE, CommonUtils.getGreenwichMeanTime().getTime()));
    feed.setLink("ST[CDATA[" + link + "END]]");
    feed.setDescription("ST[CDATA[" + desc + "END]]");
    feed.setEncoding("UTF-8");
    return feed;
  }

  private SyndEntry createNewEntry(Node questionNode, List<String> listContent) throws Exception {
    PropertyReader question = new PropertyReader(questionNode);
    SyndContent description = new SyndContentImpl();
    description.setType("text/plain");
    description.setValue("ST[CDATA[" + question.string(EXO_TITLE, EMPTY_STR) + "<br/>" + (listContent.isEmpty() ? EMPTY_STR : listContent.get(0)) + "END]]");
    SyndEntry entry = new SyndEntryImpl();
    entry.setUri("ST[CDATA[" + questionNode.getName() + "END]]");
    entry.setTitle("ST[CDATA[" + question.string(EXO_TITLE) + "END]]");
    entry.setLink("ST[CDATA[" + question.string(EXO_LINK, "http://www.exoplatform.com") + "END]]");
    entry.setContributors(listContent);
    entry.setDescription(description);
    entry.setPublishedDate(question.date(EXO_CREATED_DATE, CommonUtils.getGreenwichMeanTime().getTime()));
    entry.setAuthor("ST[CDATA[" + question.string(EXO_AUTHOR) + "END]]");
    return entry;
  }

  protected Node getFAQServiceHome(SessionProvider sProvider) throws Exception {
    String path = dataLocator.getFaqHomeLocation();
    return sessionManager.getSession(sProvider).getRootNode().getNode(path);
  }

  private Node getKSUserAvatarHomeNode(SessionProvider sProvider) throws Exception {
    String path = dataLocator.getAvatarsLocation();
    return sessionManager.getSession(sProvider).getRootNode().getNode(path);
  }

  private Node getUserSettingHome(SessionProvider sProvider) throws Exception {
    String path = dataLocator.getFaqUserSettingsLocation();
    return sessionManager.getSession(sProvider).getRootNode().getNode(path);
  }

  private Node getCategoryHome(SessionProvider sProvider, String username) throws Exception {
    String path = dataLocator.getFaqCategoriesLocation();
    return sessionManager.getSession(sProvider).getRootNode().getNode(path);
  }

  private Node getTemplateHome(SessionProvider sProvider) throws Exception {
    String path = dataLocator.getFaqTemplatesLocation();
    return sessionManager.getSession(sProvider).getRootNode().getNode(path);
  }

  private String getLastActivityInfo(String author, long answerTime) {
    return new StringBuilder(author).append(Utils.HYPHEN).append(answerTime).toString();
  }
  
  private static void logDebug(String message, Throwable e) {
    if(log.isDebugEnabled()) {
      if(e != null) {
        log.debug(message, e);
      } else {
        log.debug(message);
      }
    }
  }

  protected static void logDebug(String message) {
    logDebug(message, null);
  }
  
  @Override
  public void saveActivityIdForQuestion(String questionId, String activityId) {
    try {
      Node ownerNode = getQuestionNodeById(questionId);
      ActivityTypeUtils.attachActivityId(ownerNode, activityId);
      ownerNode.save();
    } catch (Exception e) {
      log.debug(String.format("Failed to attach activityId %s for node %s ", activityId, questionId), e);
    }
  }

  @Override
  public String getActivityIdForQuestion(String questionId) {
    try {
      Node ownerNode = getQuestionNodeById(questionId);
      return ActivityTypeUtils.getActivityId(ownerNode);
    } catch (Exception e) {
      log.debug(String.format("Failed to get attach activityId for %s ", questionId), e);
    }
    return null;
  }
  
  @Override
  public void saveActivityIdForAnswer(String questionId, Answer answer, String activityId) {
    try {
      Node ownerNode = getAnswerNode(questionId, answer);
      ActivityTypeUtils.attachActivityId(ownerNode, activityId);
      ownerNode.save();
    } catch (Exception e) {
      log.debug(String.format("Failed to attach activityId %s for node %s ", activityId, answer.getId()), e);
    }
  }

  @Override
  public String getActivityIdForAnswer(String questionId, Answer answer) {
    try {
      Node ownerNode = getAnswerNode(questionId, answer);
      return ActivityTypeUtils.getActivityId(ownerNode);
    } catch (Exception e) {
      log.debug(String.format("Failed to get attach activityId for %s ", answer.getId()), e);
    }
    return null;
  }
  
  @Override
  public void saveActivityIdForComment(String questionId, String commentId, String language, String activityId) {
    try {
      Node ownerNode = getCommentNode(questionId, commentId, language);
      ActivityTypeUtils.attachActivityId(ownerNode, activityId);
      ownerNode.save();
    } catch (Exception e) {
      log.debug(String.format("Failed to attach activityId %s for node %s ", activityId, commentId), e);
    }
  }

  @Override
  public String getActivityIdForComment(String questionId, String commentId, String language) {
    try {
      Node ownerNode = getCommentNode(questionId, commentId, language);
      return ActivityTypeUtils.getActivityId(ownerNode);
    } catch (Exception e) {
      log.debug(String.format("Failed to get attach activityId for %s ", commentId), e);
    }
    return null;
  }
  
  public Node getAnswerNode(String questionId, Answer answer) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    Node questionNode = getFAQServiceHome(sProvider).getNode(questionId);
    String defaultLang = questionNode.getProperty(EXO_LANGUAGE).getString();
    Node answerHome;
    String language = answer.getLanguage();
    if (language == null || language.equals(defaultLang)) {
      answerHome = questionNode.getNode(Utils.ANSWER_HOME);
    } else { // answer for other languages
      Node langNode = getLanguageNodeByLanguage(questionNode, answer.getLanguage());
      answerHome = langNode.getNode(Utils.ANSWER_HOME);
    }
    return answerHome.getNode(answer.getId());
  }
  
  public Node getCommentNode(String questionId, String commentId, String language) throws Exception {
    SessionProvider sProvider = CommonUtils.createSystemProvider();
    Node questionNode = getFAQServiceHome(sProvider).getNode(questionId);
    String defaultLang = questionNode.getProperty(EXO_LANGUAGE).getString();
    Node commentHome;
    if (language != null && !language.equals(defaultLang) && language.length() > 0) {
      Node languageNode = getLanguageNodeByLanguage(questionNode, language);
      commentHome = languageNode.getNode(Utils.COMMENT_HOME);
    } else {
      commentHome = questionNode.getNode(Utils.COMMENT_HOME);
    }
    return commentHome.getNode(commentId);
  }
  
}
