org.exoplatform.faq.service.impl.JCRDataStorage.java Source code

Java tutorial

Introduction

Here is the source code for org.exoplatform.faq.service.impl.JCRDataStorage.java

Source

/*
 * 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, 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 {
        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) {
        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());
            boolean isResetIndex = (isAddNew || (oldIndex != index));
            if (isResetIndex) {
                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)) {
                    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 (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);
    }

}