# QuestionReplySessionController.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.control;

    import org.silverpeas.components.questionreply.QuestionReplyException;
    import org.silverpeas.components.questionreply.model.Category;
    import org.silverpeas.components.questionreply.model.Question;
    import org.silverpeas.components.questionreply.model.QuestionDetail;
    import org.silverpeas.components.questionreply.model.Recipient;
    import org.silverpeas.components.questionreply.model.Reply;
    import org.silverpeas.components.questionreply.service.QuestionManagerProvider;
    import org.silverpeas.components.questionreply.service.notification.QuestionNotifier;
    import org.silverpeas.components.questionreply.service.notification.ReplyNotifier;
    import org.silverpeas.components.whitepages.control.CardManager;
    import org.silverpeas.components.whitepages.model.Card;
    import org.silverpeas.core.WAPrimaryKey;
    import org.silverpeas.core.admin.service.OrganizationController;
    import org.silverpeas.core.admin.user.model.SilverpeasRole;
    import org.silverpeas.core.admin.user.model.User;
    import org.silverpeas.core.admin.user.model.UserDetail;
    import org.silverpeas.core.contribution.attachment.model.Attachments;
    import org.silverpeas.core.contribution.contentcontainer.content.ContentManagementEngine;
    import org.silverpeas.core.contribution.contentcontainer.content.ContentManagerException;
    import org.silverpeas.core.contribution.contentcontainer.content.ContentManagementEngineProvider;
    import org.silverpeas.core.contribution.model.LocalizedContribution;
    import org.silverpeas.core.exception.DecodingException;
    import org.silverpeas.core.importexport.report.ExportReport;
    import org.silverpeas.core.io.upload.UploadedFile;
    import org.silverpeas.core.node.model.NodeDetail;
    import org.silverpeas.core.node.model.NodePK;
    import org.silverpeas.core.node.service.NodeService;
    import org.silverpeas.core.pdc.pdc.model.PdcClassification;
    import org.silverpeas.core.pdc.pdc.model.PdcPosition;
    import org.silverpeas.core.pdc.pdc.model.SearchContext;
    import org.silverpeas.core.pdc.pdc.service.PdcManager;
    import org.silverpeas.core.persistence.jdbc.bean.IdPK;
    import org.silverpeas.core.util.MultiSilverpeasBundle;
    import org.silverpeas.kernel.bundle.ResourceLocator;
    import org.silverpeas.kernel.bundle.SettingBundle;
    import org.silverpeas.kernel.util.StringUtil;
    import org.silverpeas.core.util.ZipUtil;
    import org.silverpeas.core.util.file.FileFolderManager;
    import org.silverpeas.core.util.file.FileRepositoryManager;
    import org.silverpeas.core.util.file.FileServerUtils;
    import org.silverpeas.kernel.logging.SilverLogger;
    import org.silverpeas.core.web.mvc.controller.AbstractComponentSessionController;
    import org.silverpeas.core.web.mvc.controller.ComponentContext;
    import org.silverpeas.core.web.mvc.controller.MainSessionController;
    import org.silverpeas.core.webapi.pdc.PdcClassificationEntity;

    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.OutputStreamWriter;
    import java.io.Writer;
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.Date;
    import java.util.List;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;

    import static java.util.Collections.singletonList;
    import static org.silverpeas.core.notification.user.builder.helper.UserNotificationHelper.buildAndSend;
    import static org.silverpeas.core.pdc.pdc.model.PdcClassification.aPdcClassificationOfContent;
    import static org.silverpeas.core.util.Charsets.UTF_8;

    public class QuestionReplySessionController extends AbstractComponentSessionController {
      private static final long serialVersionUID = -4956263179309397997L;

      private SilverpeasRole userProfil;
      private Question currentQuestion;
      private Reply currentReply;
      private Question newQuestion;
      private Reply newReply;

      /*
       * Recupère la liste des questions selon le profil de l'utilisateur courant
       */
      public Collection<Question> getQuestions() throws QuestionReplyException {
        switch (userProfil) {
          case USER:
            return getUserQuestions();
          case WRITER:
            return getWriterQuestions();
          case PUBLISHER:
            return getPublisherQuestions();
          case ADMIN:
            return getAdminQuestions();
          default:
            break;
        }
        return new ArrayList<>();
      }

      public Collection<Question> getQuestionsByCategory(String categoryId)
          throws QuestionReplyException {
        return QuestionManagerProvider.getQuestionManager()
            .getAllQuestionsByCategory(getComponentId(), categoryId);
      }

      /*
       * Recupère la question et ses réponses selon le profil de l'utilisateur courant, ainsi que ses
       * destinataires met la question en session
       */
      public Question getQuestion(long questionId) throws QuestionReplyException {
        Question question = QuestionManagerProvider.getQuestionManager().getQuestion(questionId);
        setCurrentQuestion(question);
        question.writeRecipients(
            QuestionManagerProvider.getQuestionManager().getQuestionRecipients(questionId));
        question.writeReplies(getRepliesForQuestion(questionId));
        return question;
      }

      public Collection<Reply> getRepliesForQuestion(long id) throws QuestionReplyException {
        switch (userProfil) {
          case USER:
            return getPublicRepliesForQuestion(id);
          case PUBLISHER:
            return getPrivateRepliesForQuestion(id);
          case WRITER:
          case ADMIN:
            return getAllRepliesForQuestion(id);
          default:
            break;
        }
        return new ArrayList<>();
      }

      /*
       * retourne la question courante
       */
      public Question getCurrentQuestion() {
        return this.currentQuestion;
      }

      public void setCurrentQuestion(Question question) {
        this.currentQuestion = question;
      }

      private void reSetCurrentQuestion() {
        setCurrentQuestion(null);
      }

      /*
       * Récupère une réponse met la réponse en session
       */
      public Reply getReply(long replyId) throws QuestionReplyException {
        Reply reply = QuestionManagerProvider.getQuestionManager().getReply(replyId);
        setCurrentReply(reply);
        return reply;
      }

      /*
       * Retrieve current reply
       */
      public Reply getCurrentReply() {
        return this.currentReply;
      }

      public void setCurrentReply(Reply reply) {
        WAPrimaryKey pk = reply.getPK();
        pk.setComponentName(getComponentId());
        reply.setPK(pk);
        this.currentReply = reply;
      }

      /*
       * Retourne une nouvelle question (instanceId, creatorId, creationDate) met la question en session
       * : newQuestion
       */
      public Question getNewQuestion() {
        newQuestion = new Question(getUserId(), getComponentId());
        return newQuestion;
      }

      public void setNewQuestionContent(String title, String content, String categoryId) {
        newQuestion.setTitle(title);
        newQuestion.setContent(content);
        newQuestion.setCategoryId(categoryId);
      }

      /*
       * Store the new question and notify subscribers and experts.
       */
      public long saveNewQuestion() throws QuestionReplyException {
        long questionId = QuestionManagerProvider.getQuestionManager().createQuestion(newQuestion);
        newQuestion.getPK().setId(String.valueOf(questionId));
        notifyQuestion(newQuestion);
        notifyQuestionFromExpert(newQuestion);
        return questionId;
      }

      /*
       * Retourne une nouvelle réponse (questionId, creatorId, creationDate) pour la questionCourante
       * met la question en session : newReply
       */
      public Reply getNewReply() {
        Reply reply;
        if ((getCurrentQuestion() != null) && (getCurrentQuestion().getPK() != null)) {
          reply = new Reply(((IdPK) getCurrentQuestion().getPK()).getIdAsLong(), getUserId());
        } else {
          reply = new Reply(getUserId());
        }
        newReply = reply;
        return newReply;
      }

      /*
       * initialise le contenu de la réponse à créer
       */
      public void setNewReplyContent(String title, String content, int publicReply, int privateReply) {
        newReply.setTitle(title);
        newReply.writeWysiwygContent(content);
        newReply.setPublicReply(publicReply);
        newReply.setPrivateReply(privateReply);
      }

      /**
       * Persist new FAQ inside database and add positions
       * @return question identifier
       * @throws QuestionReplyException
       */
      public long saveNewFAQ(Collection<UploadedFile> uploadedFiles) throws QuestionReplyException {
        newQuestion.setStatus(Question.CLOSED); // close
        newQuestion.setReplyNumber(1);
        newQuestion.setPublicReplyNumber(1);
        newQuestion.setPrivateReplyNumber(0);
        newReply.setPublicReply(1);
        newReply.setPrivateReply(0);
        WAPrimaryKey pk = newReply.getPK();
        pk.setComponentName(getComponentId());
        newReply.setPK(pk);

        long questionId =
            QuestionManagerProvider.getQuestionManager().createQuestionReply(newQuestion, newReply);

        addFilesToReply(uploadedFiles, newReply);

        return questionId;
      }

      private void addFilesToReply(Collection<UploadedFile> uploadedFiles, Reply reply) {
        Attachments.from(uploadedFiles).attachTo(LocalizedContribution.from(reply));
      }

      /*
       * enregistre la nouvelle réponse de la question courante met en session la question modifiée
       */
      public void saveNewReply(Collection<UploadedFile> uploadedFiles) throws QuestionReplyException {
        WAPrimaryKey pk = newReply.getPK();
        pk.setComponentName(getComponentId());
        newReply.setPK(pk);
        QuestionManagerProvider.getQuestionManager().createReply(newReply, getCurrentQuestion());

        addFilesToReply(uploadedFiles, newReply);

        getQuestion(((IdPK) getCurrentQuestion().getPK()).getIdAsLong());
        notifyReply(newReply);
      }

      public void updateCurrentQuestion(String title, String content, String categoryId)
          throws QuestionReplyException {
        getCurrentQuestion().setTitle(title);
        getCurrentQuestion().setContent(content);
        getCurrentQuestion().setCategoryId(categoryId);
        QuestionManagerProvider.getQuestionManager().updateQuestion(getCurrentQuestion());
      }

      public void updateCurrentReply(String title, String content) throws QuestionReplyException {
        Reply reply = getCurrentReply();
        reply.setTitle(title);
        reply.writeWysiwygContent(content);
        WAPrimaryKey pk = reply.getPK();
        pk.setComponentName(getComponentId());
        reply.setPK(pk);
        QuestionManagerProvider.getQuestionManager().updateReply(reply);
        getQuestion(((IdPK) getCurrentQuestion().getPK()).getIdAsLong());
      }

      /*
       * Supprime une liste de questions selon le profil de l'utilisateur courant i.e. suppression de
       * toutes les réponses publiques ou privées des questions
       */
      public void deleteQuestions(Collection<Long> questionsIds) throws QuestionReplyException {
        try {
          QuestionManagerProvider.getQuestionManager().deleteQuestionAndReplies(questionsIds);
        } catch (QuestionReplyException e) {
          throw new QuestionReplyException(e);
        }
      }

      public void deleteR(Collection<Long> replyIds) throws QuestionReplyException {
        try {
          deletePrivateReplies(replyIds);
          int rest = deletePublicReplies(replyIds);
          if (isQuestionClosedWithoutAnyReply(rest)) {
            reSetCurrentQuestion();
          } else {
            getQuestion(((IdPK) getCurrentQuestion().getPK()).getIdAsLong());
          }
        } catch (QuestionReplyException e) {
          throw new QuestionReplyException(e);
        }
      }

      private boolean isQuestionClosedWithoutAnyReply(int rest) {
        return (((getCurrentQuestion().getReplyNumber()) == 0) || (rest == 0)) &&
            (getCurrentQuestion().hasClosedStatus());
      }

      /*
       * Clos une liste de questions
       */
      public void closeQuestions(Collection<Long> questionIds) throws QuestionReplyException {
        QuestionManagerProvider.getQuestionManager().closeQuestions(questionIds);
      }

      /*
       * Clos une question si replyNumber = 0, la question sera supprimée => reSetCurrentQuestion sinon
       * met en session la question
       */
      public void closeQuestion(long questionId) throws QuestionReplyException {
        Collection<Long> questionIds = new ArrayList<>();
        questionIds.add(questionId);
        QuestionManagerProvider.getQuestionManager().closeQuestions(questionIds);
      }

      public void openQuestion(long questionId) throws QuestionReplyException {
        Collection<Long> questionIds = new ArrayList<>();
        questionIds.add(questionId);
        QuestionManagerProvider.getQuestionManager().openQuestions(questionIds);
      }

      /*
       * Supprime les réponses publiques et retourne le nombre de réponses publiques restantes.
       */
      private int deletePublicReplies(Collection<Long> replyIds) throws QuestionReplyException {
        QuestionManagerProvider.getQuestionManager()
            .updateRepliesPublicStatus(replyIds, getCurrentQuestion());
        return getCurrentQuestion().getPublicReplyNumber();
      }

      /*
       * Supprime les réponses privées =>
       * QuestionManagerProvider.getQuestionManager().updateRepliesPrivateStatus() retourne le nombre de
       * réponses privées restantes
       */
      private int deletePrivateReplies(Collection<Long> replyIds) throws QuestionReplyException {
        QuestionManagerProvider.getQuestionManager()
            .updateRepliesPrivateStatus(replyIds, getCurrentQuestion());
        return getCurrentQuestion().getPrivateReplyNumber();
      }

      /*
       * Retourne la liste des questions de l'utilisateur de rôle User i.e. liste des questions avec
       * réponses publiques => QuestionManagerProvider.getQuestionManager().getPublicQuestions()
       */
      private Collection<Question> getUserQuestions() throws QuestionReplyException {
        return QuestionManagerProvider.getQuestionManager().getPublicQuestions(getComponentId());
      }

      /*
       * Retourne la liste des questions de l'utilisateur de rôle Writer (expert) i.e. liste des
       * questions dont il est le destinataire non close =>
       * QuestionManagerProvider.getQuestionManager().getReceiveQuestions()
       */
      private Collection<Question> getWriterQuestions() throws QuestionReplyException {
        return QuestionManagerProvider.getQuestionManager()
            .getReceiveQuestions(getUserId(), getComponentId());
      }

      /*
       * Retourne la liste des questions de l'utilisateur de rôle Publisher (demandeur) i.e. liste des
       * questions dont il est l'auteur non close ou close avec réponses privées =>
       * QuestionManagerProvider.getQuestionManager().getSendQuestions()
       */
      private Collection<Question> getPublisherQuestions() throws QuestionReplyException {
        return QuestionManagerProvider.getQuestionManager()
            .getSendQuestions(getUserId(), getComponentId());
      }

      /*
       * Retourne la liste des questions de l'utilisateur de rôle Admin (animateur) i.e. liste des
       * questions non close ou close avec réponses publiques =>
       * QuestionManagerProvider.getQuestionManager().getQuestions()
       */
      private Collection<Question> getAdminQuestions() throws QuestionReplyException {
        return QuestionManagerProvider.getQuestionManager().getQuestions(getComponentId());
      }

      /*
       * liste les réponses publiques d'une question
       */
      private Collection<Reply> getPublicRepliesForQuestion(long id) throws QuestionReplyException {
        return QuestionManagerProvider.getQuestionManager()
            .getQuestionPublicReplies(id, getComponentName());
      }

      /*
       * liste les réponses privées d'une question
       */
      private Collection<Reply> getPrivateRepliesForQuestion(long id) throws QuestionReplyException {
        return QuestionManagerProvider.getQuestionManager()
            .getQuestionPrivateReplies(id, getComponentName());
      }

      /*
       * liste les réponses à une question
       */
      private Collection<Reply> getAllRepliesForQuestion(long id) throws QuestionReplyException {
        return QuestionManagerProvider.getQuestionManager().getQuestionReplies(id, getComponentName());
      }

      public final void setUserProfil() {
        this.userProfil = getHighestSilverpeasUserRole();
      }

      public void setUserProfil(String profil) {
        this.userProfil = SilverpeasRole.fromString(profil);
      }

      public String getUserProfil() {
        return this.userProfil.getName();
      }

      public SilverpeasRole getUserRole() {
        return this.userProfil;
      }

      @Override
      public SilverpeasRole getHighestSilverpeasUserRole() {
        SilverpeasRole highestUserRole = SilverpeasRole.getHighestFrom(getSilverpeasUserRoles());
        if (highestUserRole == null || SilverpeasRole.USER.isGreaterThanOrEquals(highestUserRole)) {
          highestUserRole = SilverpeasRole.USER;
        }
        return highestUserRole;
      }

      public boolean isUserExpert() {
        Collection<SilverpeasRole> userRoles = getSilverpeasUserRoles();
        return userRoles.contains(SilverpeasRole.WRITER) || userRoles.contains(SilverpeasRole.ADMIN);
      }

      /*
       * Retourne true si la liste contient deja le user
       */
      private boolean exist(UserDetail user, Collection<UserDetail> listUser) {
        if (user != null) {
          String idUser = user.getId();
          for (UserDetail currentUser : listUser) {
            if (currentUser.getId().equals(idUser)) {
              return true;
            }
          }
          return false;
        }
        return true;
      }

      /*
       * Récupère la liste des positions d'une question
       */
      public SearchContext getSilverContentIdPosition() throws QuestionReplyException {
        try {
          return PdcManager.get()
              .getSilverContentIdSearchContext(Integer.parseInt(getCurrentQuestionContentId()),
                  getComponentId());
        } catch (Exception e) {
          throw new QuestionReplyException(e);
        }
      }

      public String genericWriters() throws QuestionReplyException {
        // This method is no more used at the moment (Silverpeas 6.0)
        return "";
      }

      /*
       * Récupère la liste des experts du domaine de la question
       */
      public Collection<UserDetail> getCurrentQuestionWriters() throws QuestionReplyException {
        OrganizationController orga = getOrganisationController();
        List<UserDetail> arrayUsers = new ArrayList<>();

        try {
          ContentManagementEngine contentMgtEngine = ContentManagementEngineProvider.getContentManagementEngine();
          // recupere la liste de toutes les instances d'annuaire
          String[] instances = orga.getCompoId("whitePages");
          List<String> listeInstanceId = new ArrayList<>();
          for(String id : instances) {
            listeInstanceId.add("whitePages" + id);
          }

          // recupere la liste de tous les experts du domaine de classement de la
          // question
          SearchContext position = getSilverContentIdPosition();
          if (position != null && !position.isEmpty()) {
            PdcManager pdc = PdcManager.get();
            List<Integer> liste = pdc.findSilverContentIdByPosition(position, listeInstanceId);

            CardManager cardManager = CardManager.getInstance();
            for (Integer silverContentId : liste) {
              String internalContentId = contentMgtEngine.getInternalContentId(silverContentId);
              long userCardId = Long.parseLong(internalContentId);
              Card card = cardManager.getCard(userCardId);
              if (card != null) {
                String idUser = card.getUserId();
                UserDetail user = orga.getUserDetail(idUser);
                if (!exist(user, arrayUsers)) {
                  arrayUsers.add(user);
                }
              }
            }
          }
        } catch (Exception e) {
          throw new QuestionReplyException(e);
        }

        return arrayUsers;

      }

      /**
       * @param question the current question-reply question
       * @param recipientIds list of users to notify
       */
      private void notifyTemplateQuestion(Question question, Collection<String> recipientIds) {
        final User sender = getUserDetail(getUserId());
        buildAndSend(new QuestionNotifier(sender, question, recipientIds));
      }

      /**
       * @param question
       */
      private void notifyQuestion(Question question) {
        notifyTemplateQuestion(question,
            question.readRecipients().stream().map(Recipient::getUserId).collect(Collectors.toSet()));
      }

      /**
       * @param question
       */
      private void notifyQuestionFromExpert(Question question) {
        final List<String> profiles = singletonList(SilverpeasRole.WRITER.getName());
        String[] usersIds = getOrganisationController().getUsersIdsByRoleNames(getComponentId(), profiles);
        notifyTemplateQuestion(question, Stream.of(usersIds).collect(Collectors.toSet()));
      }

      /**
       * @param reply
       */
      private void notifyReply(final Reply reply) {
        final User sender = getUserDetail(getUserId());
        final String recipientId = getCurrentQuestion().getCreatorId();
        buildAndSend(new ReplyNotifier(sender, getCurrentQuestion(), reply, recipientId));
      }

      public QuestionReplySessionController(MainSessionController mainSessionCtrl,
          ComponentContext context, String multilangBaseName, String iconBaseName) {
        super(mainSessionCtrl, context, multilangBaseName, iconBaseName);
        setUserProfil();
      }

      public String getCurrentQuestionContentId() {
        String contentId = null;

        if (currentQuestion != null) {
          try {
            ContentManagementEngine contentMgtEngine = ContentManagementEngineProvider.getContentManagementEngine();
            contentId = String.valueOf(contentMgtEngine
                .getSilverContentId(currentQuestion.getPK().getId(), currentQuestion.getInstanceId()));
          } catch (ContentManagerException ignored) {
            SilverLogger.getLogger(this).error(ignored);
            contentId = null;
          }
        }

        return contentId;
      }

      public boolean isPrivateRepliesEnabled() {
        return "yes".equalsIgnoreCase(getComponentParameterValue("privateRepliesUsed"));
      }

      public boolean isPDCUsed() {
        if (!StringUtil.isDefined(getComponentParameterValue("usePdc"))) {
          return true;
        }
        return "yes".equalsIgnoreCase(getComponentParameterValue("usePdc"));
      }

      public Collection<NodeDetail> getAllCategories() throws QuestionReplyException {
        try {
          NodePK nodePK = new NodePK(NodePK.ROOT_NODE_ID, getComponentId());
          return getNodeService().getChildrenDetails(nodePK);
        } catch (Exception e) {
          throw new QuestionReplyException(e);
        }
      }

      public synchronized void createCategory(Category category) throws QuestionReplyException {
        try {
          category.setCreationDate(new Date());
          category.setCreatorId(getUserId());
          category.getNodePK().setComponentName(getComponentId());
          getNodeService().createNode(category, new NodeDetail());
        } catch (Exception e) {
          throw new QuestionReplyException(e);
        }
      }

      public Category getCategory(String categoryId) throws QuestionReplyException {
        try {
          NodePK nodePK = new NodePK(categoryId, getComponentId());
          return new Category(getNodeService().getDetail(nodePK));
        } catch (Exception e) {
          throw new QuestionReplyException(e);
        }
      }

      public synchronized void updateCategory(Category category) throws QuestionReplyException {
        try {
          getNodeService().setDetail(category);
        } catch (Exception e) {
          throw new QuestionReplyException(e);
        }
      }

      public synchronized void deleteCategory(String categoryId) throws QuestionReplyException {
        try {
          Collection<Question> questions = getQuestionsByCategory(categoryId);
          for (Question question : questions) {
            question.setCategoryId("");
            QuestionManagerProvider.getQuestionManager().updateQuestion(question);
          }
          NodePK nodePk = new NodePK(categoryId, getComponentId());
          getNodeService().removeNode(nodePk);
        } catch (Exception e) {
          throw new QuestionReplyException(e);
        }
      }

      public ExportReport export(MultiSilverpeasBundle resource)
          throws QuestionReplyException, ParseException {
        StringBuilder sb = new StringBuilder("exportFAQ");
        Date date = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH'H'mm'm'ss's'");
        String dateFormatee = dateFormat.format(date);
        sb.append("_").append(dateFormatee);
        sb.append("_").append(getUserDetail().getId());
        ExportReport exportReport = new ExportReport();
        // Stockage de la date de démarage de l'export dans l'objet rapport
        exportReport.setDateDebut(new Date());
        String thisExportDir = sb.toString();

        // Création du dossier d'export exportFAQ_aaaa-mm-jj-hhHmmmsss_userId.zip
        String tempDir = FileRepositoryManager.getTemporaryPath();
        File fileExportDir = new File(tempDir + thisExportDir);
        if (!fileExportDir.exists()) {
          try {
            FileFolderManager.createFolder(fileExportDir);
          } catch (org.silverpeas.core.util.UtilException ex) {
            throw new QuestionReplyException(ex);
          }
        }

        // création du dossier "files"
        String dir = tempDir + thisExportDir;
        String nameForFiles = "files";
        File forFiles = new File(dir + File.separator + nameForFiles);
        try {
          FileFolderManager.createFolder(forFiles);
        } catch (org.silverpeas.core.util.UtilException ex) {
          throw new QuestionReplyException(ex);
        }

        // intégrer la css du disque dans "files"
        SettingBundle settings = ResourceLocator.getSettingBundle(
            "org.silverpeas.questionReply.settings.questionReplySettings");
        try {
          String chemin = settings.getString("mappingDir");
          if (chemin.startsWith("file:")) {
            chemin = chemin.substring(8);
          }
          Collection<File> files = FileFolderManager.getAllFile(chemin);
          for (File file : files) {
            File newFile =
                new File(dir + File.separator + nameForFiles + File.separator + file.getName());
            FileRepositoryManager.copyFile(file.getPath(), newFile.getPath());
          }
        } catch (Exception ex) {
          throw new QuestionReplyException(ex);
        }

        // création du fichier html
        final File fileHTML = new File(dir + File.separator + thisExportDir + ".html");
        try {
          if (fileHTML.createNewFile()) {
            try(Writer fileWriter = new OutputStreamWriter(new FileOutputStream(fileHTML.getPath()), UTF_8)) {
              fileWriter.write(toHTML(fileHTML, resource));
            }
          }
        } catch (IOException ex) {
          throw new QuestionReplyException(ex);
        }

        // Création du zip
        try {
          String zipFileName = fileExportDir.getName() + ".zip";
          long zipFileSize = ZipUtil.compressPathToZip(fileExportDir.getPath(), tempDir + zipFileName);
          exportReport.setZipFileName(zipFileName);
          exportReport.setZipFileSize(zipFileSize);
          exportReport.setZipFilePath(FileServerUtils.getUrlToTempDir(zipFileName));
        } catch (Exception ex) {
          throw new QuestionReplyException(ex);
        }
        // Stockage de la date de fin de l'export dans l'objet rapport
        exportReport.setDateFin(new Date());
        return exportReport;
      }

      private String toHTML(File file, MultiSilverpeasBundle resource)
          throws QuestionReplyException, ParseException {
        String fileName = file.getName();
        StringBuilder sb = new StringBuilder();

        sb.append("<HTML>\n");
        sb.append("<HEAD>\n");

        sb.append("<TITLE>").append(fileName).append("</TITLE>\n");
        sb.append("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n");
        sb.append("<link rel=\"stylesheet\" type=\"text/css\" href=\"files/ExportFAQ.css\">\n");
        sb.append("\n");
        sb.append(addFunction());
        sb.append("\n");
        sb.append("</HEAD>\n");

        sb.append("<BODY>\n");
        sb.append("\n");
        sb.append(addBody(resource, file));
        sb.append("\n");
        sb.append("</BODY>\n");
        sb.append("</HTML>\n");

        return sb.toString();
      }

      private String addFunction() {
        StringBuilder sb = new StringBuilder();
        sb.append("<script language=\"javascript\">\n");
        sb.append("function showHideAnswer() { \n");
        sb.append("  var numericID = this.id.replace(/[^\\d]/g,'');\n");
        sb.append("  var obj = document.getElementById('a' + numericID);\n");
        sb.append("  if(obj.style.display=='block'){\n");
        sb.append("    obj.style.display='none';\n");
        sb.append("  }else{\n");
        sb.append("    obj.style.display='block';\n");
        sb.append("  }   \n");
        sb.append("}\n");

        sb.append("function initShowHideContent()\n");
        sb.append("{\n");
        sb.append("  var divs = document.getElementsByTagName('div');\n");
        sb.append("  for(var no=0;no<divs.length;no++)\n");
        sb.append("  {\n");
        sb.append("    if(divs[no].className=='question')\n");
        sb.append("    {\n");
        sb.append("      divs[no].onclick = showHideAnswer;\n");
        sb.append("    }\n");
        sb.append("  }\n");
        sb.append("}\n");

        sb.append("window.onload = initShowHideContent;\n");
        sb.append("</script>\n");
        return sb.toString();
      }

      private String addBody(MultiSilverpeasBundle resource, File file)
          throws QuestionReplyException, ParseException {
        StringBuilder sb = new StringBuilder();
        sb.append("<table width=\"100%\">\n");
        Collection<NodeDetail> categories = getAllCategories();
        QuestionReplyExport exporter = new QuestionReplyExport(resource, file);
        for (NodeDetail category : categories) {
          String categoryId = category.getId();
          exportCategory(exporter, category, categoryId, sb);
        }
        NodeDetail fakeCategory = new NodeDetail();
        fakeCategory.setName("");
        exportCategory(exporter, fakeCategory, null, sb);

        sb.append("</table>\n");
        return sb.toString();
      }

      private void exportCategory(QuestionReplyExport exporter, NodeDetail category, String categoryId,
          StringBuilder sb) throws QuestionReplyException, ParseException {
        // titre de la catégorie
        sb.append("<tr>\n");
        sb.append("<td class=\"titreCateg\" width=\"91%\">").append(category.getName())
            .append("</td>\n");
        sb.append("</tr>\n");
        // contenu de la catégorie
        sb.append("<tr>\n");
        sb.append("<td colspan=\"2\">\n");
        Collection<Question> questions = getQuestionsByCategory(categoryId);
        for (Question question : questions) {
          exporter.exportQuestion(question, sb, this);
        }
        sb.append("</td>\n");
        sb.append("</tr>\n");
      }

      private NodeService getNodeService() {
        return NodeService.get();
      }

      /**
       * Classify the question reply FAQ on the PdC only if the positions parameter is filled
       * @param questionId the question identifier
       * @param positions the json string positions
       */
      public void classifyQuestionReply(long questionId, String positions) {
        // First get the questionSilverpeasContent
        QuestionDetail questionDetail = null;
        try {
          questionDetail = new QuestionDetail(getQuestion(questionId));
        } catch (QuestionReplyException e1) {
          SilverLogger.getLogger(this).error(e1);
        }

        if (StringUtil.isDefined(positions) && questionDetail != null) {
          PdcClassificationEntity qiClassification = null;
          try {
            qiClassification = PdcClassificationEntity.fromJSON(positions);
          } catch (DecodingException e) {
            SilverLogger.getLogger(this).error(e);
          }
          if (qiClassification != null && !qiClassification.isUndefined()) {
            List<PdcPosition> pdcPositions = qiClassification.getPdcPositions();
            PdcClassification classification =
                aPdcClassificationOfContent(questionDetail).withPositions(pdcPositions);
            classification.classifyContent(questionDetail);
          }
        }
      }

    }