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 jakarta.inject.Inject;
import jakarta.transaction.Transactional;
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.QuestionDetail;
import org.silverpeas.components.questionreply.model.Recipient;
import org.silverpeas.components.questionreply.model.Reply;
import org.silverpeas.components.questionreply.repository.QuestionRepository;
import org.silverpeas.components.questionreply.repository.RecipientRepository;
import org.silverpeas.components.questionreply.repository.ReplyRepository;
import org.silverpeas.components.questionreply.service.notification.SubscriptionNotifier;
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.I18n;
import org.silverpeas.core.pdc.pdc.model.PdcPosition;
import org.silverpeas.core.persistence.jdbc.DBUtil;
import org.silverpeas.kernel.util.StringUtil;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.silverpeas.core.notification.user.builder.helper.UserNotificationHelper.buildAndSend;
import static org.silverpeas.core.pdc.pdc.model.PdcClassification.aPdcClassificationOfContent;
@Service
public class SilverpeasQuestionManager implements QuestionManager {
@Inject
private QuestionRepository questionRepository;
@Inject
private ReplyRepository replyRepository;
@Inject
private RecipientRepository recipientRepository;
@Inject
private QuestionIndexer questionIndexer;
@Inject
private QuestionReplyContentManager contentManager;
@Inject
private OrganizationController controller;
@Inject
private I18n i18n;
/**
* 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
@Transactional
public long createQuestion(Question question) throws QuestionReplyException {
Collection<Recipient> recipients = question.readRecipients();
questionRepository.save(question);
long questionId = question.getIdAsLong();
questionIndexer.createIndex(question, List.of());
for (Recipient recipient : recipients) {
recipient.setQuestionId(questionId);
recipientRepository.save(recipient);
}
try (Connection con = DBUtil.openConnection()) {
contentManager.createSilverContent(con, question);
} catch (SQLException | ContentManagerException e) {
throw new QuestionReplyException(e);
}
return questionId;
}
/*
* enregistre une réponse à une question : met à jour publicReplyNumber et/ou privateReplyNumber
* et replyNumber de la question ainsi que le status à 1.
*/
@Override
@Transactional
public long createReply(Reply reply, Question question) throws QuestionReplyException {
reply.setQuestion(question);
replyRepository.save(reply);
WysiwygController.createFileAndAttachment(reply.readCurrentWysiwygContent(),
reply.toResourceReference(), reply.getCreatorId(), i18n.getDefaultLanguage());
if (question.hasNewStatus()) {
question.waitForAnswer();
}
updateQuestion(question);
notifySubscribers(question, reply);
return reply.getIdAsLong();
}
@Override
@Transactional
public void closeQuestions(Collection<Long> questionIds, String instanceId)
throws QuestionReplyException {
for (Question question : getQuestionsOfFaq(questionIds, instanceId)) {
question.close();
updateQuestion(question);
}
}
@Override
@Transactional
public void openQuestions(Collection<Long> questionIds, String instanceId)
throws QuestionReplyException {
for (Question question : getQuestionsOfFaq(questionIds, instanceId)) {
question.waitForAnswer();
updateQuestion(question);
}
}
/*
* Affecte le status public à 0 d'une liste de réponses : updateReply décrémente le nombre de
* réponses publiques de la question d'autant : updateQuestion
*/
@Override
@Transactional
public void updateRepliesPublicStatus(Collection<Long> replyIds, Question question)
throws QuestionReplyException {
if (replyIds != null) {
for (Reply reply : getReplies(replyIds, question)) {
reply.setPublicReply(0);
updateReplyStatus(reply);
}
updateQuestion(question);
}
}
/*
* Affecte le status private à 0 d'une liste de réponses : updateReply décrémente le nombre de
* réponses privées de la question d'autant : updateQuestion
*/
@Override
@Transactional
public void updateRepliesPrivateStatus(Collection<Long> replyIds, Question question)
throws QuestionReplyException {
if (replyIds != null) {
for (Reply reply : getReplies(replyIds, question)) {
reply.setPrivateReply(0);
updateReplyStatus(reply);
}
updateQuestion(question);
}
}
/*
* Modifie une question. La question est supprimée si publicReplyNumber et privateReplyNumber
* sont à 0 et que la question est fermée. Met à jour publicReplyNumber et/ou privateReplyNumber
* et replyNumber de la question
*/
@Override
@Transactional
public void updateQuestion(Question question) throws QuestionReplyException {
try {
long questionId = question.getIdAsLong();
question.setReplyNumber(replyRepository.countByQuestion(questionId));
question.setPublicReplyNumber(replyRepository.countPublicByQuestion(questionId));
question.setPrivateReplyNumber(replyRepository.countPrivateByQuestion(questionId));
if ((question.getReplyNumber() == 0) && question.hasClosedStatus()) {
deleteQuestion(question);
} else {
questionRepository.save(question);
questionIndexer.updateIndex(question, getAllReplies(questionId, question.getInstanceId()));
contentManager.updateSilverContentVisibility(question);
}
} catch (ContentManagerException 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
@Transactional
public void updateReply(Reply reply) throws QuestionReplyException {
Question question = reply.getQuestion();
if ((reply.getPublicReply() == 0) && (reply.getPrivateReply() == 0)) {
replyRepository.delete(List.of(reply));
} else {
replyRepository.save(reply);
updateWysiwygContent(reply);
}
questionIndexer.updateIndex(question,
getAllReplies(question.getIdAsLong(), question.getInstanceId()));
}
@Override
@Transactional
public void deleteQuestionAndReplies(Collection<Long> questionIds, String instanceId)
throws QuestionReplyException {
for (Question question : getQuestionsOfFaq(questionIds, instanceId)) {
long questionId = question.getIdAsLong();
recipientRepository.deleteByQuestion(questionId);
replyRepository.delete(getAllReplies(questionId, instanceId));
questionIndexer.deleteIndex(question);
questionRepository.delete(List.of(question));
deleteSilverContent(question);
}
}
@Override
public List<Reply> getAllReplies(long questionId, String instanceId)
throws QuestionReplyException {
List<Reply> allReplies = new ArrayList<>(getQuestionPrivateReplies(questionId, instanceId));
allReplies.addAll(getQuestionPublicReplies(questionId, instanceId));
return allReplies;
}
@Override
public Optional<String> getInstanceIdOfQuestion(long questionId) {
return questionRepository.getInstanceIdOf(questionId);
}
@Override
public Question getQuestion(long questionId, String instanceId) {
return questionRepository.getById(questionId, instanceId);
}
@Override
public List<Question> getQuestionsByIds(List<String> ids, String instanceId) {
return questionRepository.getById(
ids.stream().map(Integer::parseInt).collect(Collectors.toSet()), instanceId);
}
/*
* récupère la liste des réponses d'une question
*/
@Override
public List<Reply> getQuestionReplies(long questionId, String instanceId) {
return withWysiwygContent(replyRepository.findByQuestion(questionId, instanceId));
}
/*
* récupère la liste des réponses publiques d'une question
*/
@Override
public List<Reply> getQuestionPublicReplies(long questionId, String instanceId) {
return withWysiwygContent(replyRepository.findPublicByQuestion(questionId, instanceId));
}
/*
* récupère la liste des réponses privées d'une question
*/
@Override
public List<Reply> getQuestionPrivateReplies(long questionId, String instanceId) {
return withWysiwygContent(replyRepository.findPrivateByQuestion(questionId, instanceId));
}
/*
* récupère la liste des destinataires d'une question
*/
@Override
public List<Recipient> getQuestionRecipients(long questionId) {
return recipientRepository.findByQuestion(questionId);
}
/*
* récupère une réponse
*/
@Override
public Reply getReply(long replyId, String instanceId) {
Reply reply = replyRepository.getById(replyId, instanceId);
if (reply != null) {
reply.loadWysiwygContent();
}
return reply;
}
/*
* Récupère la liste des questions émises par un utilisateur et qui ne soient pas fermées ou
* fermées avec réponses privées
*/
@Override
public List<Question> getSendQuestions(String userId, String instanceId) {
return questionRepository.findSentBy(userId, instanceId);
}
/*
* Récupère la liste des questions recues par un utilisateur, et qui ne sont pas fermées
*/
@Override
public List<Question> getReceiveQuestions(String userId, String instanceId) {
return questionRepository.findReceivedBy(userId, instanceId);
}
/*
* Récupère la liste des questions qui ne sont pas closes ou closes avec réponses publiques
*/
@Override
public List<Question> getQuestions(String instanceId) {
return questionRepository.findNotClosedOrWithPublicReplies(instanceId);
}
/*
* Récupère la liste de toutes les questions avec toutes ses réponses
*/
@Override
public List<Question> getAllQuestions(String instanceId) {
List<Question> questions = getQuestions(instanceId).stream()
.map(this::withReplies)
.collect(Collectors.toList());
if (isSortable(instanceId)) {
questions.sort(QuestionRegexpComparator.getInstance());
}
return questions;
}
@Override
public List<Question> getAllQuestionsByCategory(String instanceId, String categoryId) {
List<Question> questions = getQuestions(instanceId).stream()
.filter(question -> isInCategory(question, categoryId))
.map(this::withReplies)
.collect(Collectors.toList());
if (isSortable(instanceId)) {
questions.sort(QuestionRegexpComparator.getInstance());
}
return questions;
}
/*
* Récupère la liste des questions publiques avec réponses
*/
@Override
public List<Question> getPublicQuestions(String instanceId) {
return questionRepository.findWithPublicReplies(instanceId);
}
/**
* 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 {
return createQuestionReply(question, reply, List.of());
}
@Override
@Transactional
public long createQuestionReply(Question question, Reply reply, List<PdcPosition> positions)
throws QuestionReplyException {
questionRepository.save(question);
reply.setQuestion(question);
replyRepository.save(reply);
WysiwygController.createFileAndAttachment(reply.readCurrentWysiwygContent(),
reply.toResourceReference(), reply.getCreatorId(), i18n.getDefaultLanguage());
questionIndexer.createIndex(question, List.of(reply));
try (Connection con = DBUtil.openConnection()) {
contentManager.createSilverContent(con, question);
} catch (SQLException | ContentManagerException e) {
throw new QuestionReplyException(e);
}
classify(question, positions);
notifySubscribers(question, reply);
return question.getIdAsLong();
}
protected void updateWysiwygContent(Reply reply) {
if (WysiwygController.haveGotWysiwyg(reply.getInstanceId(), reply.getId(),
i18n.getDefaultLanguage())) {
WysiwygController.updateFileAndAttachment(reply.readCurrentWysiwygContent(),
reply.getInstanceId(), reply.getId(), reply.getCreatorId(), i18n.getDefaultLanguage());
} else {
WysiwygController.createUnindexedFileAndAttachment(reply.readCurrentWysiwygContent(),
reply.toResourceReference(), reply.getCreatorId(), i18n.getDefaultLanguage());
}
}
protected boolean isSortable(String instanceId) {
return StringUtil
.getBooleanValue(controller.getComponentParameterValue(instanceId, "sortable"));
}
private List<Question> getQuestionsOfFaq(Collection<Long> questionIds, String instanceId) {
if (questionIds == null) {
return List.of();
}
return questionRepository.getById(
questionIds.stream().map(Long::intValue).collect(Collectors.toSet()), instanceId);
}
private List<Reply> getReplies(Collection<Long> replyIds, Question question) {
return replyRepository.getById(
replyIds.stream().map(Long::intValue).collect(Collectors.toSet()),
question.getIdAsLong());
}
private List<Reply> withWysiwygContent(List<Reply> replies) {
replies.forEach(Reply::loadWysiwygContent);
return replies;
}
private Question withReplies(Question question) {
question.writeReplies(getQuestionReplies(question.getIdAsLong(), question.getInstanceId()));
return question;
}
private static boolean isInCategory(Question question, String categoryId) {
return (StringUtil.isNotDefined(question.getCategoryId()) &&
StringUtil.isNotDefined(categoryId)) ||
(StringUtil.isDefined(categoryId) && categoryId.equals(question.getCategoryId()));
}
/*
* Modifie le statut d'une réponse : la réponse est supprimée si le status public et le status
* private sont à 0.
*/
private void updateReplyStatus(Reply reply) throws QuestionReplyException {
Question question = reply.getQuestion();
if ((reply.getPublicReply() == 0) && (reply.getPrivateReply() == 0)) {
replyRepository.delete(List.of(reply));
WysiwygController.deleteFile(reply.getInstanceId(), reply.getId(),
i18n.getDefaultLanguage());
} else {
replyRepository.save(reply);
updateWysiwygContent(reply);
}
questionIndexer.updateIndex(question,
getAllReplies(question.getIdAsLong(), question.getInstanceId()));
}
private void deleteQuestion(Question question) throws QuestionReplyException {
recipientRepository.deleteByQuestion(question.getIdAsLong());
questionRepository.delete(List.of(question));
questionIndexer.deleteIndex(question);
deleteSilverContent(question);
}
private void deleteSilverContent(Question question) throws QuestionReplyException {
try (Connection con = DBUtil.openConnection()) {
contentManager.deleteSilverContent(con, question);
} catch (SQLException | ContentManagerException e) {
throw new QuestionReplyException(e);
}
}
/**
* Classifies the specified question on the PdC. The subscribers on the PdC aren't alerted about
* the classification: they are notified, with the other subscribers, about the replies to the
* question.
*/
private void classify(Question question, List<PdcPosition> positions) {
if (!positions.isEmpty()) {
final QuestionDetail questionDetail = new QuestionDetail(question);
aPdcClassificationOfContent(questionDetail).withPositions(positions)
.classifyContent(questionDetail, false);
}
}
private void notifySubscribers(Question question, Reply reply) {
if (reply.getPublicReply() == 1) {
final User sender = reply.readAuthor();
buildAndSend(new SubscriptionNotifier(sender, question, reply));
}
}
}