DefaultBlogService.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:
* "https://www.silverpeas.org/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.blog.service;
import org.silverpeas.components.blog.BlogContentManager;
import org.silverpeas.components.blog.dao.PostDAO;
import org.silverpeas.components.blog.model.Archive;
import org.silverpeas.components.blog.model.BlogRuntimeException;
import org.silverpeas.components.blog.model.Category;
import org.silverpeas.components.blog.model.PostDetail;
import org.silverpeas.components.blog.notification.BlogUserSubscriptionNotification;
import org.silverpeas.core.ResourceReference;
import org.silverpeas.core.annotation.Service;
import org.silverpeas.core.comment.model.Comment;
import org.silverpeas.core.comment.service.CommentService;
import org.silverpeas.core.contribution.content.wysiwyg.service.WysiwygController;
import org.silverpeas.core.contribution.contentcontainer.content.ContentManagerException;
import org.silverpeas.core.contribution.model.ContributionIdentifier;
import org.silverpeas.core.contribution.publication.model.Location;
import org.silverpeas.core.contribution.publication.model.PublicationDetail;
import org.silverpeas.core.contribution.publication.model.PublicationPK;
import org.silverpeas.core.contribution.publication.service.PublicationService;
import org.silverpeas.core.html.PermalinkRegistry;
import org.silverpeas.core.index.search.SearchEngineProvider;
import org.silverpeas.core.index.search.model.MatchingIndexEntry;
import org.silverpeas.core.index.search.model.QueryDescription;
import org.silverpeas.core.initialization.Initialization;
import org.silverpeas.core.node.model.NodeDetail;
import org.silverpeas.core.node.model.NodeOrderComparator;
import org.silverpeas.core.node.model.NodePK;
import org.silverpeas.core.node.service.NodeService;
import org.silverpeas.core.notification.user.builder.helper.UserNotificationHelper;
import org.silverpeas.core.pdc.pdc.model.PdcClassification;
import org.silverpeas.core.persistence.jdbc.DBUtil;
import org.silverpeas.core.subscription.SubscriptionService;
import org.silverpeas.core.subscription.SubscriptionServiceProvider;
import org.silverpeas.core.subscription.service.ComponentSubscription;
import org.silverpeas.core.util.DateUtil;
import org.silverpeas.kernel.bundle.LocalizationBundle;
import org.silverpeas.kernel.bundle.ResourceLocator;
import org.silverpeas.kernel.bundle.SettingBundle;
import org.silverpeas.kernel.util.StringUtil;
import org.silverpeas.kernel.logging.SilverLogger;
import jakarta.annotation.Nonnull;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.transaction.Transactional;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
import java.util.stream.Stream;
import static java.util.stream.Collectors.*;
import static org.silverpeas.core.SilverpeasExceptionMessages.*;
import static org.silverpeas.core.contribution.publication.dao.PublicationCriteria.onComponentInstanceIds;
/**
* Default implementation of the services provided by the Blog component. It is managed by the
* underlying IoC container. At initialization by the IoC container, it registers itself among
* different services for which it is interested.
*/
@Service
@Named("blogService")
public class DefaultBlogService implements BlogService, Initialization {
private static final String MESSAGES_PATH = "org.silverpeas.blog.multilang.blogBundle";
private static final String SETTINGS_PATH = "org.silverpeas.blog.settings.blogSettings";
private static final SettingBundle settings = ResourceLocator.getSettingBundle(SETTINGS_PATH);
private static final String POST = "post";
private static final String USELESS = "useless";
@Inject
private CommentService commentService;
@Inject
private BlogContentManager blogContentManager;
@Inject
private NodeService nodeService;
@Inject
private PublicationService publicationService;
@Inject
private PostDAO postDAO;
@Override
public void init() {
PermalinkRegistry.get()
.addUrlPart("Post");
}
@Override
public Optional<PostDetail> getContributionById(ContributionIdentifier contributionId) {
PublicationDetail publication = publicationService.getDetail(
new PublicationPK(contributionId.getLocalId(), contributionId.getComponentInstanceId()));
return publication == null ?
Optional.empty() :
toPosts(publication.getInstanceId(), List.of(publication)).stream().findFirst();
}
@Override
public SettingBundle getComponentSettings() {
return settings;
}
@Override
public LocalizationBundle getComponentMessages(String language) {
return ResourceLocator.getLocalizationBundle(MESSAGES_PATH, language);
}
@Override
public boolean isRelatedTo(final String instanceId) {
return instanceId.startsWith("blog");
}
private Map<String, Date> getEventDateIndexedByPost(Collection<String> pubIds) {
try {
return postDAO.getEventDateIndexedByPost(pubIds);
} catch (SQLException e) {
throw new BlogRuntimeException(failureOnGetting(POST, pubIds), e);
}
}
@Transactional
@Override
public String createPost(PostDetail post) {
return createPost(post, null);
}
@Transactional
@Override
public String createPost(final PostDetail post, PdcClassification classification) {
try (Connection con = openConnection()) {
// Create publication
PublicationDetail pub = post.getPublication();
pub.setStatus(PublicationDetail.DRAFT_STATUS);
PublicationPK pk = publicationService.createPublication(pub);
// Create post
postDAO.create(con, pk.getId(), post.getDateEvent(), pk.getInstanceId());
if (StringUtil.isDefined(post.getCategoryId())) {
setCategory(pk, post.getCategoryId());
}
// Create empty wysiwyg content
WysiwygController.createUnindexedFileAndAttachment("", new ResourceReference(pk),
pub.getCreatorId(), pub.getLanguage());
// Create silver content
createSilverContent(con, pub, pub.getCreatorId());
// classify the publication on the PdC if its classification is defined. The subscribers
// are notified later about the post itself, once it is published
if (classification != null) {
classification.classifyContent(pub, false);
}
return pk.getId();
} catch (Exception e) {
throw new BlogRuntimeException(failureOnAdding(POST, post.getId()), e);
}
}
@Override
public void sendSubscriptionsNotification(final NodePK fatherPK, final PostDetail post,
final Comment comment, final String type, final String senderId) {
// the subscribers to the positions on the PdC on which the post is classified are notified
// with the subscribers of the blog
UserNotificationHelper.buildAndSend(
new BlogUserSubscriptionNotification(post, comment, type, senderId));
}
@Transactional
@Override
public void updatePost(PostDetail post, final PdcClassification classification) {
try (Connection con = openConnection()) {
PublicationPK pubPk = post.getPublication()
.getPK();
PublicationDetail pub = post.getPublication();
// Remove last category
publicationService.removeAllFathers(pubPk);
// Save the publication
publicationService.setDetail(pub);
// Add the new category
if (StringUtil.isDefined(post.getCategoryId())) {
setCategory(pubPk, post.getCategoryId());
}
// Update event date
postDAO.update(con, pubPk.getId(), post.getDateEvent());
// Save wysiwyg content and do not index it (cause it is already indexed as publication
// content)
WysiwygController.updateFileAndAttachment(post.getContent(), pub.getInstanceId(),
pubPk.getId(), pub.getUpdaterId(), pub.getLanguage(), false);
// classify the publication on the PdC if any
if (classification != null) {
classification.classifyContentOrClearClassificationIfEmpty(pub, false);
}
// Send notification if subscription
if (pub.isValid()) {
sendSubscriptionsNotification(new NodePK("0", pub.getPK()
.getSpaceId(), pub.getPK()
.getInstanceId()), post, null, "update", pub.getUpdaterId());
}
} catch (SQLException e) {
throw new BlogRuntimeException(failureOnUpdate(POST, post.getId()), e);
}
}
private void setCategory(PublicationPK pk, String categoryId) {
NodePK nodePK = new NodePK(categoryId, pk.getInstanceId());
publicationService.addFather(pk, nodePK);
}
@Transactional
@Override
public void deletePost(String instanceId, String postId) {
try (Connection con = openConnection()) {
PublicationPK pubPK = new PublicationPK(postId, instanceId);
// Delete link with category
publicationService.removeAllFathers(pubPK);
// Delete date event
postDAO.delete(con, pubPK.getId());
// Delete comments
ResourceReference resourceReference = new ResourceReference(postId, instanceId);
getCommentService().deleteAllCommentsOnResource(PostDetail.getResourceType(),
resourceReference);
// Delete wysiwyg content
WysiwygController.deleteFileAndAttachment(instanceId, postId);
// Delete publication
publicationService.deletePublication(pubPK);
// Delete silverContent
blogContentManager.deleteSilverContent(con, pubPK);
} catch (Exception e) {
throw new BlogRuntimeException(failureOnDeleting(POST, postId), e);
}
}
private Connection openConnection() {
Connection con;
try {
con = DBUtil.openConnection();
} catch (SQLException e) {
// traitement des exceptions
throw new BlogRuntimeException(failureOnOpeningConnectionTo("datasource"), e);
}
return con;
}
@Nonnull
private List<PostDetail> toPosts(final String instanceId,
final Collection<PublicationDetail> publications) {
if (!publications.isEmpty()) {
final Map<Location, Category> categoryByAllNodePKs = nodeService.getAllNodes(
new NodePK(null, instanceId))
.stream()
.collect(toMap(n -> new Location(n.getId(), instanceId), Category::new));
final Collection<String> pubIds = publications.stream()
.map(PublicationDetail::getPK)
.map(PublicationPK::getId)
.collect(toSet());
final Map<String, Category> allLocationsByPublicationIds =
publicationService.getAllLocationsByPublicationIds(pubIds)
.entrySet()
.stream()
.collect(toMap(Map.Entry::getKey, e -> categoryByAllNodePKs.get(e.getValue().get(0))));
final Map<String, Date> eventDateByPost = getEventDateIndexedByPost(pubIds);
final Map<String, Integer> commentsByPubIds =
getCommentService().getCommentCountIndexedByResource(
PostDetail.getResourceType(), instanceId)
.entrySet()
.stream()
.collect(toMap(e -> e.getKey().getLocalId(), Map.Entry::getValue));
final Date defaultDate = new Date();
return publications.stream().map(p -> {
final String pubId = p.getPK().getId();
final Category cat = allLocationsByPublicationIds.get(pubId);
final Date dateEvent = eventDateByPost.getOrDefault(pubId, defaultDate);
final int nbComments = commentsByPubIds.getOrDefault(pubId, 0);
final PostDetail post = new PostDetail(p, cat, nbComments, dateEvent);
//noinspection removal
post.setCreatorName(p.getCreator().getDisplayedName());
return post;
}).collect(toList());
}
return List.of();
}
@Override
public Collection<PostDetail> getLastPosts(final String instanceId, final BlogFilters filters) {
try (Connection con = openConnection()) {
// search publications sorted by event date
Collection<String> allPostIds = postDAO.getAllPostIds(con, instanceId);
final Stream<PublicationDetail> publications = filterPublications(
publicationService.getByIds(allPostIds).stream(), filters);
return toPosts(instanceId, publications.collect(toList()));
} catch (Exception e) {
throw new BlogRuntimeException(failureOnGetting("all posts of blog", instanceId), e);
}
}
@Override
public Collection<PostDetail> getAllPosts(String instanceId) {
return getLastPosts(instanceId, new BlogFilters(true));
}
@Override
public Collection<PostDetail> getLastValidPosts(String instanceId, BlogFilters filters) {
try (Connection con = openConnection()) {
// rechercher les publications classées par date d'évènement
final Collection<String> allPostIds = postDAO.getAllPostIds(con, instanceId);
final Stream<PublicationDetail> publications = publicationService.getByIds(allPostIds)
.stream()
.filter(p -> PublicationDetail.VALID_STATUS.equals(p.getStatus()));
return toPosts(instanceId, filterPublications(publications, filters).collect(toList()));
} catch (Exception e) {
throw new BlogRuntimeException(failureOnGetting("All validated posts for blog", instanceId),
e);
}
}
@Override
public Collection<PostDetail> getPostsByCategory(String instanceId, String categoryId,
final BlogFilters filters) {
try (Connection con = openConnection()) {
// looking for classified publications
final Collection<String> allPostIds = postDAO.getAllPostIds(con, instanceId);
final Map<String, PublicationDetail> categoryPublications =
publicationService.getPublicationsByCriteria(
onComponentInstanceIds(instanceId).onNodes(categoryId))
.stream()
.collect(toMap(PublicationDetail::getId, p -> p));
final Stream<PublicationDetail> publications = allPostIds.stream()
.map(categoryPublications::get)
.filter(Objects::nonNull);
return toPosts(instanceId, filterPublications(publications, filters).collect(toList()));
} catch (Exception e) {
throw new BlogRuntimeException(failureOnGetting("all posts in category", categoryId), e);
}
}
@Override
public Collection<PostDetail> getPostsByEventDate(String instanceId, String date,
final BlogFilters filters) {
return getPostsByArchive(instanceId, date, date, filters);
}
@Override
public Collection<PostDetail> getPostsByArchive(String instanceId, String beginDate, String endDate,
final BlogFilters filters) {
try (Connection con = openConnection()) {
// searching publications sorted by event date
final Collection<String> postIds = postDAO.getPostInRange(con, instanceId, beginDate,
endDate);
final List<PublicationDetail> publications = publicationService.getByIds(postIds);
return toPosts(instanceId,
filterPublications(publications.stream(), filters).collect(toList()));
} catch (Exception e) {
throw new BlogRuntimeException(
failureOnGetting("all posts archived between", beginDate + " and " + endDate), e);
}
}
@Override
public Collection<PostDetail> getResultSearch(String instanceId, String word, String userId, final BlogFilters filters) {
QueryDescription query = new QueryDescription(word);
query.setSearchingUser(userId);
query.addComponent(instanceId);
try (Connection con = openConnection()) {
final Set<String> searchResult = SearchEngineProvider.getSearchEngine()
.search(query)
.getEntries()
.stream()
.map(MatchingIndexEntry::getObjectId)
.collect(toSet());
// création des billets à partir des résultats
// rechercher la liste des posts triée par date
final Collection<String> searchedPostIds = postDAO.getAllPostIds(con, instanceId)
.stream()
.filter(searchResult::contains)
.collect(toList());
final List<PublicationDetail> publications = filterPublications(
publicationService.getByIds(searchedPostIds).stream(), filters).collect(toList());
return toPosts(instanceId, publications);
} catch (Exception e) {
throw new BlogRuntimeException(e);
}
}
@Transactional
@Override
public void createCategory(Category category) {
try {
nodeService.createNode(category, new NodeDetail());
} catch (Exception e) {
throw new BlogRuntimeException(e);
}
}
@Transactional
@Override
public void updateCategory(Category category) {
try {
nodeService.setDetail(category);
} catch (Exception e) {
throw new BlogRuntimeException(e);
}
}
@Transactional
@Override
public void deleteCategory(String instanceId, String id) {
try {
NodePK nodePk = new NodePK(id, instanceId);
// recherche des billets sur cette catégorie
Collection<PostDetail> posts = getPostsByCategory(instanceId, id, new BlogFilters(true));
for (PostDetail post : posts) {
publicationService.removeFather(post.getPublication()
.getPK(), nodePk);
}
// suppression de la catégorie
nodeService.deleteNode(nodePk);
} catch (Exception e) {
throw new BlogRuntimeException(e);
}
}
@Override
public Category getCategory(NodePK pk) {
return new Category(nodeService.getDetail(pk));
}
@Override
public Collection<NodeDetail> getAllCategories(String instanceId) {
NodePK nodePK = new NodePK(NodePK.ROOT_NODE_ID, instanceId);
List<NodeDetail> result = new ArrayList<>(nodeService.getChildrenDetails(nodePK));
result.sort(new NodeOrderComparator());
return result;
}
@Override
public Collection<Archive> getAllArchives(String instanceId) {
try (Connection con = openConnection()) {
final Calendar calendar = Calendar.getInstance(Locale.FRENCH);
return postDAO.getAllEventDates(con, instanceId)
.stream()
.map(d -> {
calendar.setTime(d);
return createArchive(calendar);
})
.distinct()
.collect(toList());
} catch (Exception e) {
throw new BlogRuntimeException(failureOnGetting("All archives of blog", instanceId), e);
}
}
private Archive createArchive(Calendar calendar) {
Date beginDate = getMonthFirstDay(calendar);
Date endDate = getMonthLastDay(calendar);
// regarder s'il y a des événements sur cette période
Archive archive =
new Archive(USELESS, DateUtil.date2SQLDate(beginDate), DateUtil.date2SQLDate(endDate));
archive.setYear(java.lang.Integer.toString(calendar.get(Calendar.YEAR)));
archive.setMonthId(java.lang.Integer.toString(calendar.get(Calendar.MONTH)));
return archive;
}
@Override
public void indexBlog(String componentId) {
indexTopics(new NodePK(USELESS, componentId));
indexPublications(componentId);
}
private void indexPublications(String componentId) {
Collection<PublicationDetail> pubs;
try {
pubs = publicationService.getAllPublications(componentId);
} catch (Exception e) {
throw new BlogRuntimeException(
failureOnGetting("[INDEXING] all publications in blog", componentId), e);
}
if (pubs != null) {
for (PublicationDetail pub : pubs) {
try {
indexPublication(pub.getPK());
} catch (Exception e) {
throw new BlogRuntimeException(failureOnIndexing("publication", pub.getPK()
.toString()), e);
}
}
}
}
private void indexPublication(PublicationPK pubPK) {
// index publication itself
publicationService.createIndex(pubPK);
// index external elements
indexExternalElementsOfPublication(pubPK);
}
private void indexTopics(NodePK nodePK) {
Collection<NodeDetail> nodes = nodeService.getAllNodes(nodePK);
if (nodes != null) {
for (NodeDetail node : nodes) {
if (!node.getNodePK()
.isRoot() && !node.getNodePK()
.isTrash()) {
nodeService.createIndex(node);
}
}
}
}
@Override
public boolean isSubscribed(final String userId, final String instanceId) {
return getSubscribeService().existsSubscription(new ComponentSubscription(userId, instanceId));
}
private void indexExternalElementsOfPublication(PublicationPK pubPK) {
try {
// index comments
getCommentService().indexAllCommentsOnPublication(PostDetail.getResourceType(),
new ResourceReference(pubPK));
} catch (Exception e) {
SilverLogger.getLogger(this)
.error("Comment index failure for publication {0}", new String[]{pubPK.toString()}, e);
}
}
private Date getMonthFirstDay(Calendar calendar) {
calendar.set(Calendar.DAY_OF_MONTH, 1);
return calendar.getTime();
}
private Date getMonthLastDay(Calendar calendar) {
int monthLastDay = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
calendar.set(Calendar.DAY_OF_MONTH, monthLastDay);
return calendar.getTime();
}
private void createSilverContent(Connection con, PublicationDetail pubDetail, String creatorId) {
try {
blogContentManager.createSilverContent(con, pubDetail, creatorId);
} catch (ContentManagerException e) {
throw new BlogRuntimeException(e);
}
}
private void updateSilverContentVisibility(PublicationDetail pubDetail) {
try {
blogContentManager.updateSilverContentVisibility(pubDetail);
} catch (Exception e) {
throw new BlogRuntimeException(e);
}
}
@Override
public void draftOutPost(PostDetail post) {
PublicationDetail pub = post.getPublication();
pub.setStatus(PublicationDetail.VALID_STATUS);
// update the publication
publicationService.setDetail(pub);
if (pub.getStatus()
.equals(PublicationDetail.VALID_STATUS)) {
// update visibility attribute on PDC
updateSilverContentVisibility(pub);
// send notification if instance subscription
sendSubscriptionsNotification(new NodePK("0", pub.getPK()
.getSpaceId(), pub.getPK()
.getInstanceId()), post, null, "create", pub.getUpdaterId());
}
}
private Stream<PublicationDetail> filterPublications(final Stream<PublicationDetail> pubs,
final BlogFilters filters) {
Stream<PublicationDetail> stream = pubs.filter(filters.toPredicate());
final Optional<Integer> maxResult = filters.getMaxResult();
if (maxResult.isPresent()) {
stream = stream.limit(maxResult.get());
}
return stream;
}
private SubscriptionService getSubscribeService() {
return SubscriptionServiceProvider.getSubscribeService();
}
/**
* Gets a DefaultCommentService instance.
* @return a DefaultCommentService instance.
*/
protected CommentService getCommentService() {
return commentService;
}
}