# SilverpeasQuestionManager.java

    /*
     * Copyright (C) 2000 - 2026 Silverpeas
     *
     * 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.
     *
     * As a special exception to the terms and conditions of version 3.0 of
     * the GPL, you may redistribute this Program in connection with Free/Libre
     * Open Source Software ("FLOSS") applications as described in Silverpeas's
     * FLOSS exception. You should have received a copy of the text describing
     * the FLOSS exception, and it is also available here:
     * "http://www.silverpeas.org/docs/core/legal/floss_exception.html"
     *
     * 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 Affero General Public License for more details.
     *
     * You should have received a copy of the GNU Affero General Public License
     * along with this program. If not, see <https://www.gnu.org/licenses/>.
     */
    package org.silverpeas.components.questionreply.service;

    import org.silverpeas.components.questionreply.QuestionReplyException;
    import org.silverpeas.components.questionreply.index.QuestionIndexer;
    import org.silverpeas.components.questionreply.model.Question;
    import org.silverpeas.components.questionreply.model.Recipient;
    import org.silverpeas.components.questionreply.model.Reply;
    import org.silverpeas.components.questionreply.service.notification.SubscriptionNotifier;
    import org.silverpeas.core.ResourceReference;
    import org.silverpeas.core.WAPrimaryKey;
    import org.silverpeas.core.admin.service.OrganizationController;
    import org.silverpeas.core.admin.user.model.User;
    import org.silverpeas.core.annotation.Service;
    import org.silverpeas.core.contribution.content.wysiwyg.service.WysiwygController;
    import org.silverpeas.core.contribution.contentcontainer.content.ContentManagerException;
    import org.silverpeas.core.i18n.I18NHelper;
    import org.silverpeas.core.persistence.jdbc.DBUtil;
    import org.silverpeas.core.persistence.jdbc.bean.*;
    import org.silverpeas.kernel.util.StringUtil;
    import org.silverpeas.kernel.logging.SilverLogger;

    import javax.inject.Inject;
    import java.sql.Connection;
    import java.sql.SQLException;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.Collections;
    import java.util.List;
    import java.util.stream.Collectors;

    import static org.silverpeas.core.notification.user.builder.helper.UserNotificationHelper.buildAndSend;
    import static org.silverpeas.core.persistence.jdbc.bean.BeanCriteria.OPERATOR.GREATER;
    import static org.silverpeas.core.persistence.jdbc.bean.BeanCriteria.OPERATOR.NOT_EQUALS;

    @Service
    @SuppressWarnings("deprecation")
    public class SilverpeasQuestionManager implements QuestionManager {

      private static final String STATUS = "status";
      private static final String INSTANCE_ID = "instanceId";
      private static final String QUESTION_ID = "questionId";
      @Inject
      private QuestionIndexer questionIndexer;
      private SilverpeasBeanDAO<Question> questionDao = null;
      private SilverpeasBeanDAO<Reply> replyDao = null;
      private SilverpeasBeanDAO<Recipient> recipientDao = null;
      @Inject
      private QuestionReplyContentManager contentManager;
      @Inject
      private OrganizationController controller;

      SilverpeasQuestionManager() {
        try {
          questionDao = SilverpeasBeanDAOFactory.getDAO(Question.class);
          replyDao = SilverpeasBeanDAOFactory.getDAO(Reply.class);
          recipientDao = SilverpeasBeanDAOFactory.getDAO(Recipient.class);
        } catch (PersistenceException ex) {
          SilverLogger.getLogger(this).error(ex);
        }
      }

      /**
       * Create and persist a question with targeted recipient (be careful recipient doesn't have
       * question identifier set)
       * @param question the question to create
       * @return the generated question identifier
       * @throws QuestionReplyException if an error occurs
       */
      @Override
      public long createQuestion(Question question) throws QuestionReplyException {
        try (Connection con = DBUtil.openConnection()) {
          Collection<Recipient> recipients = question.readRecipients();
          IdPK pkQ = (IdPK) questionDao.add(con, question);
          question.setPK(pkQ);
          questionIndexer.createIndex(question, Collections.emptyList());
          long idQ = pkQ.getIdAsLong();
          if (recipients != null) {
            for (Recipient recipient : recipients) {
              recipient.setQuestionId(idQ);
              createRecipient(con, recipient);
            }
          }
          question.setPK(pkQ);
          contentManager.createSilverContent(con, question);
          return idQ;
        } catch (SQLException | ContentManagerException | PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * enregistre une réponse à une question => met à jour publicReplyNumber et/ou privateReplyNumber
       * et replyNumber de la question ainsi que le status à 1
       */
      @Override
      public long createReply(Reply reply, Question question) throws QuestionReplyException {
        try (Connection con = DBUtil.openConnection()) {
          IdPK pkR = (IdPK) replyDao.add(con, reply);
          WysiwygController.createFileAndAttachment(reply.readCurrentWysiwygContent(),
              new ResourceReference(pkR), reply.getCreatorId(), I18NHelper.DEFAULT_LANGUAGE);
          long idR = pkR.getIdAsLong();
          if (question.hasNewStatus()) {
            question.waitForAnswer();
          }
          updateQuestion(con, question);
          questionIndexer.updateIndex(question, getAllReplies(reply.getQuestionId(), question.
              getInstanceId()));
          notifySubscribers(question, reply);
          return idR;
        } catch (SQLException | PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * enregistre un destinataire
       */
      private void createRecipient(Connection con, Recipient recipient) throws QuestionReplyException {
        try {
          recipientDao.add(con, recipient);
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * supprime tous les destinataires d'une question
       */
      private void deleteRecipients(Connection con, long questionId) throws QuestionReplyException {
        try {
          BeanCriteria criteria = BeanCriteria.addCriterion(QUESTION_ID, questionId);
          recipientDao.removeBy(con, criteria);
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * Clos une liste de questions : updateQuestion
       */
      @Override
      public void closeQuestions(Collection<Long> questionIds) throws QuestionReplyException {
        if (questionIds != null) {
          try (Connection con = DBUtil.openConnection()) {
            for (Long idQ : questionIds) {
              Question question = getQuestion(idQ);
              question.close();
              updateQuestion(con, question);
            }
          } catch (SQLException e) {
            throw new QuestionReplyException(e);
          }
        }
      }

      @Override
      public void openQuestions(Collection<Long> questionIds) throws QuestionReplyException {
        if (questionIds != null) {
          try (Connection con = DBUtil.openConnection()) {
            for (Long idQ : questionIds) {
              Question question = getQuestion(idQ);
              question.waitForAnswer();
              updateQuestion(con, question);
            }
          } catch (SQLException e) {
            throw new QuestionReplyException(e);
          }
        }
      }

      /*
       * Modifie les destinataires d'une question : deleteRecipients, createRecipient
       */
      @Override
      public void updateQuestionRecipients(Question question) throws QuestionReplyException {
        try (Connection con = DBUtil.openConnection()) {
          Collection<Recipient> recipients = question.readRecipients();
          deleteRecipients(con, ((IdPK) question.getPK()).getIdAsLong());
          if (recipients != null) {
            for (Recipient recipient : recipients) {
              recipient.setQuestionId(((IdPK) question.getPK()).getIdAsLong());
              createRecipient(con, recipient);
            }
          }
        } catch (SQLException | QuestionReplyException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * Affecte le status public à 0 de toutes les réponses d'une liste de questions : updateReply
       * Affecte le nombre de réponses publiques de la question à 0 : updateQuestion si question en
       * attente, on a demandé à la supprimer : deleteQuestion
       */
      @Override
      public void updateQuestionRepliesPublicStatus(Collection<Long> questionIds)
          throws QuestionReplyException {
        try (Connection con = DBUtil.openConnection()) {
          if (questionIds != null) {
            for (Long idQ : questionIds) {
              Question question = getQuestion(idQ);
              Collection<Reply> replies = getQuestionPublicReplies(idQ, question.getInstanceId());
              if (replies != null) {
                for (Reply reply : replies) {
                  reply.setPublicReply(0);
                  addComponentId(reply, question.getPK().getInstanceId());
                  updateReply(con, reply);
                }
                updateQuestion(con, question);
              }
              if (question.hasNewStatus()) {
                deleteQuestion(con, idQ);
              }
            }
          }
        } catch (SQLException | QuestionReplyException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * Affecte le status private à 0 de toutes les réponses d'une liste de questions : updateReply
       * Affecte le nombre de réponses privées de la question à 0 : updateQuestion
       */
      @Override
      public void updateQuestionRepliesPrivateStatus(Collection<Long> questionIds)
          throws QuestionReplyException {
        try (Connection con = DBUtil.openConnection()) {
          if (questionIds != null) {
            for (Long idQ : questionIds) {
              Question question = getQuestion(idQ);
              Collection<Reply> replies = getQuestionPrivateReplies(idQ, question.getInstanceId());
              if (replies != null) {
                for (Reply reply : replies) {
                  reply.setPrivateReply(0);
                  addComponentId(reply, question.getPK().getInstanceId());
                  updateReply(con, reply);
                }
                updateQuestion(con, question);
              }
            }
          }
        } catch (SQLException | QuestionReplyException e) {
          throw new QuestionReplyException(e);
        }
      }

      private void addComponentId(Reply reply, String componentId) {
        reply.getPK().setComponentName(componentId);
      }

      /*
       * Affecte le status public à 0 d'une liste de réponses : updateReply Décremente le nombre de
       * réponses publiques de la question d'autant : updateQuestion
       */
      @Override
      public void updateRepliesPublicStatus(Collection<Long> replyIds, Question question)
          throws QuestionReplyException {
        try (Connection con = DBUtil.openConnection()) {
          if (replyIds != null) {
            for (Long idR : replyIds) {
              Reply reply = getReply(idR);
              if (reply != null) {
                reply.setPublicReply(0);
                addComponentId(reply, question.getPK().getInstanceId());
                updateReply(con, reply);
              }
            }
            updateQuestion(con, question);
          }
        } catch (SQLException | QuestionReplyException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * Affecte le status private à 0 d'une liste de réponses : updateReply Décremente le nombre de
       * réponses privées de la question d'autant : updateQuestion
       */
      @Override
      public void updateRepliesPrivateStatus(Collection<Long> replyIds, Question question)
          throws QuestionReplyException {
        try (Connection con = DBUtil.openConnection()) {
          if (replyIds != null) {
            for (Long idR : replyIds) {
              Reply reply = getReply(idR);
              if (reply != null) {
                reply.setPrivateReply(0);
                addComponentId(reply, question.getPK().getInstanceId());
                updateReply(con, reply);
              }
            }
            updateQuestion(con, question);
          }
        } catch (SQLException | QuestionReplyException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * Modifie une question => la question est supprimée si publicReplyNumber et privateReplyNumber
       * sont à 0 et que la question est close => met à jour publicReplyNumber et/ou privateReplyNumber
       * et replyNumber de la question
       */
      private void updateQuestion(Connection con, Question question) throws QuestionReplyException {
        try {
          long idQ = ((IdPK) question.getPK()).getIdAsLong();
          question.setReplyNumber(getQuestionRepliesNumber(idQ));
          question.setPublicReplyNumber(getQuestionPublicRepliesNumber(idQ));
          question.setPrivateReplyNumber(getQuestionPrivateRepliesNumber(idQ));
          if ((question.getReplyNumber() == 0) && question.hasClosedStatus()) {
            deleteQuestion(con, idQ);
          } else {
            questionDao.update(con, question);
            questionIndexer.updateIndex(question, getAllReplies(idQ, question.getInstanceId()));
            question.getPK().setComponentName(question.getInstanceId());
            contentManager.updateSilverContentVisibility(question);
          }
        } catch (Exception e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * Modifie une question => la question est supprimée si publicReplyNumber et privateReplyNumber
       * sont à 0 et que la question est close => met à jour publicReplyNumber et/ou privateReplyNumber
       * et replyNumber de la question
       */
      @Override
      public void updateQuestion(Question question) throws QuestionReplyException {
        try {
          long idQ = ((IdPK) question.getPK()).getIdAsLong();
          question.setReplyNumber(getQuestionRepliesNumber(idQ));
          question.setPublicReplyNumber(getQuestionPublicRepliesNumber(idQ));
          question.setPrivateReplyNumber(getQuestionPrivateRepliesNumber(idQ));
          if ((question.getReplyNumber() == 0) && question.hasClosedStatus()) {
            deleteQuestion(idQ);
          } else {
            questionDao.update(question);
            questionIndexer.updateIndex(question, getAllReplies(idQ, question.getInstanceId()));
            question.getPK().setComponentName(question.getInstanceId());
            contentManager.updateSilverContentVisibility(question);
          }
        } catch (Exception e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * Modifie une réponse => La réponse est supprimée si le status public et le status private sont à
       * 0
       */
      private void updateReply(Connection con, Reply reply) throws QuestionReplyException {
        try {
          Question question = getQuestion(reply.getQuestionId());
          reply.getPK().setComponentName(question.getInstanceId());
          if ((reply.getPublicReply() == 0) && (reply.getPrivateReply() == 0)) {
            deleteReply(con, reply.getPK());
          } else {
            replyDao.update(con, reply);
            updateWysiwygContent(reply);
          }
          questionIndexer.updateIndex(question, getAllReplies(reply.getQuestionId(), question.
              getInstanceId()));
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * Modifie une réponse => La réponse est supprimée si le status public et le status private sont à
       * 0
       */
      @Override
      public void updateReply(Reply reply) throws QuestionReplyException {
        try {
          Question question = getQuestion(reply.getQuestionId());
          if ((reply.getPublicReply() == 0) && (reply.getPrivateReply() == 0)) {
            deleteReply(((IdPK) reply.getPK()).getIdAsLong());
          } else {
            replyDao.update(reply);
            updateReplyWysiwygContent(reply);
          }
          questionIndexer.updateIndex(question, getAllReplies(reply.getQuestionId(), question.
              getInstanceId()));
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      private void updateReplyWysiwygContent(final Reply reply) {
        updateWysiwygContent(reply);
      }

      private void deleteQuestion(Connection con, long questionId) throws QuestionReplyException {
        try {
          deleteRecipients(con, questionId);
          IdPK pk = new IdPK();
          pk.setIdAsLong(questionId);
          Question question = getQuestion(questionId);
          String peasId = question.getInstanceId();
          questionDao.remove(con, pk);
          questionIndexer.deleteIndex(question);
          pk.setComponentName(peasId);
          contentManager.deleteSilverContent(con, pk);
        } catch (Exception e) {
          throw new QuestionReplyException(e);
        }
      }

      private void deleteQuestion(long questionId) throws QuestionReplyException {
        try(Connection connection = DBUtil.openConnection()) {
          deleteQuestion(connection, questionId);
        } catch (SQLException e) {
          throw new QuestionReplyException(e);
        }
      }

      @Override
      public void deleteQuestionAndReplies(Collection<Long> questionIds) throws QuestionReplyException {
        // pour chaque question
        for (Long questionId : questionIds) {
          Connection con = null;
          try {
            con = DBUtil.openConnection();
            deleteRecipients(con, questionId);
            IdPK pk = new IdPK();
            pk.setIdAsLong(questionId);
            Question question = getQuestion(questionId);
            String peasId = question.getInstanceId();
            // rechercher les réponses
            Collection<Reply> replies = getAllReplies(questionId, peasId);
            for (Reply reply : replies) {
              long replyId = Long.parseLong(reply.getPK().getId());
              addComponentId(reply, question.getInstanceId());
              // supprimer la réponse et son index
              deleteReply(replyId);
            }
            questionIndexer.deleteIndex(question);
            // supprimer la question
            questionDao.remove(con, pk);
            pk.setComponentName(peasId);
            contentManager.deleteSilverContent(con, pk);
          } catch (Exception e) {
            throw new QuestionReplyException(e);
          } finally {
            DBUtil.close(con);
          }

        }
      }

      @Override
      public List<Reply> getAllReplies(long questionId, String instanceId)
          throws QuestionReplyException {
        List<Reply> allReplies = new ArrayList<>();
        try {
          Collection<Reply> privateReplies = getQuestionPrivateReplies(questionId, instanceId);
          allReplies.addAll(privateReplies);
          Collection<Reply> publicReplies = getQuestionPublicReplies(questionId, instanceId);
          allReplies.addAll(publicReplies);
          return allReplies;
        } catch (Exception e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * supprime une réponse
       */
      private void deleteReply(long replyId) throws QuestionReplyException {
        try (Connection con = DBUtil.openConnection()) {
          IdPK pk = new IdPK();
          pk.setIdAsLong(replyId);
          replyDao.remove(con, pk);
        } catch (SQLException | PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * supprime une réponse
       */
      private void deleteReply(Connection con, WAPrimaryKey replyId) throws QuestionReplyException {
        try {
          replyDao.remove(con, replyId);
          WysiwygController
              .deleteFile(replyId.getInstanceId(), replyId.getId(), I18NHelper.DEFAULT_LANGUAGE);
        } catch (PersistenceException e) {

          throw new QuestionReplyException(e);
        }
      }

      /*
       * recupère une question
       */
      @Override
      public Question getQuestion(long questionId) throws QuestionReplyException {
        try {
          IdPK pk = new IdPK();
          pk.setIdAsLong(questionId);
          return questionDao.findByPrimaryKey(pk);
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      @Override
      public Question getQuestionAndReplies(long questionId) throws QuestionReplyException {
        try {
          IdPK pk = new IdPK();
          pk.setIdAsLong(questionId);
          Question question = questionDao.findByPrimaryKey(pk);
          Collection<Reply> replies = getQuestionReplies(questionId, question.getInstanceId());
          question.writeReplies(replies);
          return question;
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      @Override
      public List<Question> getQuestionsByIds(List<String> ids) throws QuestionReplyException {
        BeanCriteria criteria = ids.isEmpty() ? BeanCriteria.emptyCriteria() :
            BeanCriteria.addCriterion("id",
                ids.stream().map(Integer::parseInt).collect(Collectors.toSet()));
        try {
          return new ArrayList<>(questionDao.findBy(criteria));
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * recupère la liste des réponses d'une question
       */
      @Override
      public List<Reply> getQuestionReplies(long questionId, String instanceId)
          throws QuestionReplyException {
        try {
          BeanCriteria criteria = BeanCriteria.addCriterion(QUESTION_ID, questionId);
          List<Reply> replies = new ArrayList<>(
              replyDao.findBy(criteria));
          for (Reply reply : replies) {
            reply.getPK().setComponentName(instanceId);
            reply.loadWysiwygContent();
          }
          return replies;
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * recupère la liste des réponses publiques d'une question
       */
      @Override
      public List<Reply> getQuestionPublicReplies(long questionId, String instanceId)
          throws QuestionReplyException {
        try {
          BeanCriteria criteria = BeanCriteria.addCriterion("publicReply", 1)
              .and(QUESTION_ID, questionId);
          List<Reply> replies = new ArrayList<>(replyDao.findBy(criteria));
          for (Reply reply : replies) {
            reply.getPK().setComponentName(instanceId);
            reply.loadWysiwygContent();
          }
          return replies;
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * recupère la liste des réponses privées d'une question
       */
      @Override
      public List<Reply> getQuestionPrivateReplies(long questionId, String instanceId)
          throws QuestionReplyException {
        try {
          BeanCriteria criteria = BeanCriteria.addCriterion("privateReply", 1)
              .and(QUESTION_ID, questionId);
          List<Reply> replies = new ArrayList<>(replyDao.findBy(criteria));
          for (Reply reply : replies) {
            reply.getPK().setComponentName(instanceId);
            reply.loadWysiwygContent();
          }
          return replies;
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * recupère la liste des destinataires d'une question
       */
      @Override
      public List<Recipient> getQuestionRecipients(long questionId) throws QuestionReplyException {
        try {
          BeanCriteria criteria = BeanCriteria.addCriterion(QUESTION_ID, questionId);
          return new ArrayList<>(recipientDao.findBy(criteria));
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * recupère une réponse
       */
      @Override
      public Reply getReply(long replyId) throws QuestionReplyException {
        try {
          IdPK pk = new IdPK();
          pk.setIdAsLong(replyId);
          Reply reply = replyDao.findByPrimaryKey(pk);
          if (reply != null) {
            reply.loadWysiwygContent();
          }
          return reply;
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * Recupère la liste des questions emises par un utilisateur => Q dont il est l'auteur qui ne sont
       * pas closes ou closes avec réponses privées
       */
      @Override
      public List<Question> getSendQuestions(String userId, String instanceId)
          throws QuestionReplyException {
        try {
          BeanCriteria criteria = BeanCriteria.addCriterion(INSTANCE_ID, instanceId)
              .and("creatorId", Integer.parseInt(userId))
              .and(BeanCriteria.addCriterion(STATUS, NOT_EQUALS,  Question.CLOSED)
                  .or("privateReplyNumber", GREATER, 0));
          return new ArrayList<>(questionDao.findBy(criteria));
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * Recupère la liste des questions recues par un utilisateur => Q dont il est le destinataire et
       * qui ne sont pas closes
       */
      @Override
      public List<Question> getReceiveQuestions(String userId, String instanceId)
          throws QuestionReplyException {
        try {
          BeanCriteria criteria = BeanCriteria.addCriterion(INSTANCE_ID, instanceId)
              .and(STATUS, NOT_EQUALS, Question.CLOSED)
              .andSubQuery("id", BeanCriteria.OPERATOR.IN,
                  "questionId from SC_QuestionReply_Recipient",
                  BeanCriteria.addCriterion("userId", Integer.parseInt(userId)));
          return new ArrayList<>(questionDao.findBy(criteria));
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * Recupère la liste des questions qui ne sont pas closes ou closes avec réponses publiques
       */
      @Override
      public List<Question> getQuestions(String instanceId) throws QuestionReplyException {
        try {
          BeanCriteria criteria = BeanCriteria.addCriterion(INSTANCE_ID, instanceId)
              .and(BeanCriteria.addCriterion(STATUS, NOT_EQUALS,  Question.CLOSED)
                  .or("publicReplyNumber", GREATER, 0));
          criteria.setDescOrderBy("creationDate", "id");
          return new ArrayList<>(questionDao.findBy(criteria));
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * Recupère la liste de toutes les questions avec toutes ses réponses
       */
      @Override
      public List<Question> getAllQuestions(String instanceId) throws QuestionReplyException {
        List<Question> allQuestions = getQuestions(instanceId);
        List<Question> questions = new ArrayList<>(allQuestions.size());
        for (Question question : allQuestions) {
          Question fullQuestion = getQuestionAndReplies(Long.parseLong(question.getPK().getId()));
          questions.add(fullQuestion);
        }
        if (isSortable(instanceId)) {
          questions.sort(QuestionRegexpComparator.getInstance());
        }
        return questions;
      }

      @Override
      public List<Question> getAllQuestionsByCategory(String instanceId, String categoryId)
          throws QuestionReplyException {
        List<Question> allQuestions = getQuestions(instanceId);
        List<Question> questions = new ArrayList<>(allQuestions.size());
        for (Question question : allQuestions) {
          if ((StringUtil.isNotDefined(question.getCategoryId()) &&
              StringUtil.isNotDefined(categoryId)) ||
              (StringUtil.isDefined(categoryId) && StringUtil.isDefined(question.getCategoryId()) &&
                  question.getCategoryId().equals(categoryId))) {
            // la question est sans catégorie
            Question fullQuestion = getQuestionAndReplies(Long.parseLong(question.getPK().getId()));
            questions.add(fullQuestion);
          }
        }
        if (isSortable(instanceId)) {
          questions.sort(QuestionRegexpComparator.getInstance());
        }
        return questions;
      }

      /*
       * Recupère la liste des questions publiques avec réponses
       */
      @Override
      public List<Question> getPublicQuestions(String instanceId) throws QuestionReplyException {
        try {
          BeanCriteria criteria = BeanCriteria.addCriterion(INSTANCE_ID, instanceId)
              .and("publicReplyNumber", GREATER, 0);
          criteria.setAscOrderBy("id");
          return new ArrayList<>(questionDao.findBy(criteria));
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /**
       * Save and persist question and reply given in parameter
       * @param question the new question
       * @param reply the answer linked to the given question
       * @return the created question identifier
       * @throws QuestionReplyException if an error occurs
       */
      @Override
      public long createQuestionReply(Question question, Reply reply) throws QuestionReplyException {
        Connection con = null;
        long idQ;
        try {
          con = DBUtil.openConnection();
          IdPK pkQ = (IdPK) questionDao.add(con, question);
          idQ = pkQ.getIdAsLong();
          reply.setQuestionId(idQ);
          WAPrimaryKey pkR = replyDao.add(con, reply);
          reply.getPK().setId(pkR.getId());
          WysiwygController.createFileAndAttachment(reply.readCurrentWysiwygContent(),
              new ResourceReference(pkR), reply.getCreatorId(), I18NHelper.DEFAULT_LANGUAGE);
          questionIndexer.createIndex(question, Collections.singletonList(reply));
          Question updatedQuestion = getQuestion(idQ);
          contentManager.createSilverContent(con, updatedQuestion);
          notifySubscribers(question, reply);
        } catch (Exception e) {
          throw new QuestionReplyException(e);
        } finally {
          DBUtil.close(con);
        }
        return idQ;
      }

      /*
       * recupère le nombre de réponses d'une question
       */
      private int getQuestionRepliesNumber(long questionId) throws QuestionReplyException {
        try {
          BeanCriteria criteria = BeanCriteria.addCriterion(QUESTION_ID, questionId);
          Collection<Reply> replies = replyDao.findBy(criteria);
          return replies.size();
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * recupère le nombre de réponses publiques d'une question
       */
      private int getQuestionPublicRepliesNumber(long questionId) throws QuestionReplyException {
        try {
          BeanCriteria criteria = BeanCriteria.addCriterion("publicReply", 1)
              .and(QUESTION_ID, questionId);
          Collection<Reply> replies =
              replyDao.findBy(criteria);
          return replies.size();
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      /*
       * recupère le nombre de réponses privées d'une question
       */
      private int getQuestionPrivateRepliesNumber(long questionId) throws QuestionReplyException {
        try {
          BeanCriteria criteria = BeanCriteria.addCriterion("privateReply", 1)
              .and(QUESTION_ID, questionId);
          Collection<Reply> replies = replyDao.findBy(criteria);
          return replies.size();
        } catch (PersistenceException e) {
          throw new QuestionReplyException(e);
        }
      }

      protected void updateWysiwygContent(Reply reply) {
        if (WysiwygController.haveGotWysiwyg(reply.getPK().getInstanceId(), reply.getPK().getId(),
            I18NHelper.DEFAULT_LANGUAGE)) {
          WysiwygController
              .updateFileAndAttachment(reply.readCurrentWysiwygContent(), reply.getPK().getInstanceId(),
                  reply.getPK().getId(), reply.getCreatorId(), I18NHelper.DEFAULT_LANGUAGE);
        } else {
          WysiwygController.createUnindexedFileAndAttachment(reply.readCurrentWysiwygContent(),
              new ResourceReference(reply.getPK()),
                  reply.getCreatorId(), I18NHelper.DEFAULT_LANGUAGE);
        }
      }

      protected boolean isSortable(String instanceId) {
        return StringUtil
            .getBooleanValue(controller.getComponentParameterValue(instanceId, "sortable"));
      }

      private void notifySubscribers(Question question, Reply reply) {
        if (reply.getPublicReply() == 1) {
          final User sender = reply.readAuthor();
          buildAndSend(new SubscriptionNotifier(sender, question, reply));
        }
      }
    }