QuestionReplySessionController.java

/*
 * Copyright (C) 2000 - 2024 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);
      }
    }
  }

}