ContentServiceImpl.java
/*
* Copyright 2019 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.server.service.impl;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.genesys.blocks.auditlog.service.ClassPKService;
import org.genesys.blocks.model.ClassPK;
import org.genesys.blocks.model.EntityId;
import org.genesys.filerepository.InvalidRepositoryFileDataException;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.genesys.filerepository.model.RepositoryFile;
import org.genesys.filerepository.service.RepositoryService;
import org.genesys.server.api.v1.mapper.APIv1Mapper;
import org.genesys.server.api.v1.model.ActivityPost;
import org.genesys.server.api.v1.model.Article;
import org.genesys.server.api.v1.model.Menu;
import org.genesys.server.api.v1.model.MenuItem;
import org.genesys.server.api.v1.model.RepositoryImage;
import org.genesys.server.api.v1.model.RepositoryImageInfo;
import org.genesys.server.component.security.AsAdminInvoker;
import org.genesys.server.component.security.SecurityUtils;
import org.genesys.server.exception.NotFoundElement;
import org.genesys.server.exception.SearchException;
import org.genesys.server.model.UserRole;
import org.genesys.server.model.impl.ArticleLang;
import org.genesys.server.persistence.MenuRepository;
import org.genesys.server.service.ActivityPostService;
import org.genesys.server.service.ArticleService;
import org.genesys.server.service.ArticleTranslationService;
import org.genesys.server.service.CRMException;
import org.genesys.server.service.ContentService;
import org.genesys.server.service.MenuItemService;
import org.genesys.server.service.filter.ActivityPostFilter;
import org.genesys.server.service.filter.ArticleFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.multipart.MultipartFile;
@Service
@Validated
public class ContentServiceImpl implements ContentService {
public static final Logger LOG = LoggerFactory.getLogger(ContentServiceImpl.class);
@Autowired
private ClassPKService classPkService;
@Autowired
private MenuRepository repoMenu;
@Autowired
private VelocityEngine velocityEngine;
@Autowired
protected RepositoryService repositoryService;
/** The securityUtils. */
@Autowired
private SecurityUtils securityUtils;
/** Execute code as admin */
@Autowired
protected AsAdminInvoker asAdminInvoker;
@Autowired
private ActivityPostService activityPostService;
@Autowired
private ArticleService articleService;
@Autowired
private APIv1Mapper mapper;
@Autowired
private ArticleTranslationService articleTranslationService;
@Autowired
private MenuItemService menuItemService;
@Override
public Locale getDefaultLocale() {
return Locale.ENGLISH;
}
@Override
@Transactional(readOnly = true)
public List<ActivityPost> lastNews() {
final PageRequest page = PageRequest.of(0, 6, Direction.DESC, "publishDate");
List<ActivityPost> allNews = allNews(page).getContent();
return allNews;
}
@Override
@Transactional(readOnly = true)
public Page<ActivityPost> allNews(Pageable page) {
return activityPostService.allNews(page).map(mapper::map);
}
/**
* @throws SearchException
* @deprecated use {@link #listArticles(ArticleFilter filter, Pageable pageable)}
*/
@Override
@Deprecated
@Transactional(readOnly = true)
public Page<Article> listArticles(Pageable pageable) throws SearchException {
return articleService.listFiltered(null, pageable).map(mapper::map);
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Transactional(readOnly = true)
public Page<Article> listArticles(final ArticleFilter filter, final Pageable pageable) throws SearchException {
return articleService.listFiltered(filter, pageable).map(mapper::map);
}
/**
* @throws SearchException
* @deprecated use {@link #listArticles(ArticleFilter filter, Pageable pageable)}
*/
@Override
@Deprecated
@Transactional(readOnly = true)
public Page<Article> listArticlesByLang(String lang, Pageable pageable) throws SearchException {
var filter = new ArticleFilter();
filter.originalLanguageTag = Set.of(lang);
return articleService.listFiltered(filter, pageable).map(mapper::map);
}
@Override
@Transactional(readOnly = true)
public Article getGlobalArticle(String slug, Locale locale, boolean useDefault) {
return getArticle(Article.class, null, slug, locale, useDefault);
}
/**
* Get article, use default locale if required
*/
@Override
@Transactional(readOnly = true)
public Article getGlobalArticle(String slug, Locale locale) {
return getGlobalArticle(slug, locale, true);
}
@Override
@Transactional(readOnly = true)
public Article getArticle(EntityId entity, String slug, Locale locale) {
return getArticle(entity.getClass(), entity.getId(), slug, locale, true);
}
@Override
@Transactional(readOnly = true)
public Article getArticleBySlugAndLang(String slug, String lang) {
// QArticle qArticle = QArticle.article;
// var inst = Instant.now();
// if (! (securityUtils.hasRole(UserRole.ADMINISTRATOR) || securityUtils.hasRole(UserRole.CONTENTMANAGER))) {
// return articleRepository.findOne(qArticle.publishDate.before(inst)
// .andAnyOf(qArticle.expirationDate.isNull(), qArticle.expirationDate.after(inst))
// .and(qArticle.slug.eq(slug)).and(qArticle.lang.eq(lang))).orElse(null);
// }
// return articleRepository.findBySlugAndLang(slug, lang);
return getArticle(Article.class, null, slug, Locale.forLanguageTag(lang), false);
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Transactional(readOnly = true)
public Article getArticleBySlugLangTargetIdClassPk(String slug, String lang, Long targetId, String classPKShortName) {
// ClassPK classPK = classPkService.findByShortName(classPKShortName);
// return articleRepository.findBySlugAndLangAndTargetIdAndClassPk(slug, lang, targetId, classPK);
var article = articleService.getArticleBySlugLangTargetIdClassPk(slug, lang, targetId, classPKShortName);
return mapper.map(article);
}
@Override
@Transactional(readOnly = true)
public Article getArticle(Class<?> clazz, Long targetId, String slug, Locale locale, boolean useDefault) {
var article = articleService.getArticleBySlugLangTargetIdClassPk(slug, locale.toLanguageTag(), targetId, clazz.getSimpleName());
return mapper.map(article);
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Transactional
public Article updateArticle(long id, String slug, String title, String summary, String body) throws NotFoundElement {
var lang = LocaleContextHolder.getLocale().toLanguageTag();
// return article;
var article = articleService.get(id);
if (Objects.equals(article.getOriginalLanguageTag(), lang)) {
// Update Article
article.setSlug(slug);
article.setTitle(title);
article.setSummary(summary);
article.setBody(body);
article = articleService.update(article);
} else {
// Update ArticleLang
var articleLang = articleTranslationService.getLang(article, lang);
if (articleLang == null) {
articleLang = new ArticleLang(article, lang, title, summary, body);
} else {
articleLang.setTitle(title);
articleLang.setSummary(summary);
articleLang.setBody(body);
}
articleTranslationService.upsertLang(article, articleLang);
}
return mapper.map(articleService.loadTranslated(article.getId()));
}
@Override
@Transactional
public Article createArticle(Class<?> clazz, Long targetId, String slug, Locale locale, String title, String summary, String body, boolean isTemplate, Instant publishDate, Instant expirationDate) throws CRMException {
var article = articleService.getArticle(clazz, targetId, slug, locale);
if (article == null) {
var art = new org.genesys.server.model.impl.Article();
art.setClassPk(getClassPk(clazz));
art.setTargetId(targetId);
art.setSlug(slug);
art.setOriginalLanguageTag(locale.toLanguageTag());
art.setTemplate(isTemplate);
art.setTitle(title);
art.setSummary(summary);
art.setBody(body);
art.setPublishDate(publishDate);
art.setExpirationDate(expirationDate);
art = articleService.createFast(art);
return mapper.map(articleService.loadTranslated(art.getId()));
}
if (Objects.equals(locale.toLanguageTag(), article.entity.getOriginalLanguageTag())) {
// Not a translation!
return mapper.map(article);
} else {
var translation = article.translation;
if (translation == null) { // No such translation
translation = new ArticleLang(article.entity, locale.toLanguageTag(), title, summary, body);
// } else {
// translation.setTitle(title);
// translation.setSummary(summary);
// translation.setBody(body);
articleTranslationService.upsertLang(article.entity, translation);
}
return mapper.map(articleService.loadTranslated(article.entity.getId()));
}
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Transactional
public Article updateGlobalArticle(String slug, Locale locale, String title, String summary, String body, Instant publishDate, Instant expirationDate) throws CRMException {
var lang = locale.toLanguageTag();
var article = articleService.getArticleBySlugAndLang(slug, lang);
var translation = article.translation;
if (Objects.equals(lang, article.entity.getOriginalLanguageTag())) {
article.entity.setTitle(title);
article.entity.setSummary(summary);
article.entity.setBody(body);
article.entity.setPublishDate(publishDate);
article.entity.setExpirationDate(expirationDate);
articleService.update(article.entity);
} else {
if (translation == null) { // No such translation
translation = new ArticleLang(article.entity, lang, title, summary, body);
} else {
translation.setTitle(title);
translation.setSummary(summary);
translation.setBody(body);
}
articleTranslationService.upsertLang(article.entity, translation);
}
return mapper.map(articleService.loadTranslated(article.entity.getId()));
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Transactional
public Article updateGlobalArticle(String slug, Locale locale, String title, String summary, String body) throws CRMException {
return updateGlobalArticle(slug, locale, title, summary, body, Instant.now(), null);
}
/**
* Creates or updates an article
*
* @param entity
* @param body
* @param locale
* @return
* @throws CRMException
*/
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER') or hasPermission(#entity, 'ADMINISTRATION')")
@Transactional
public Article updateArticle(EntityId entity, String slug, String title, String summary, String body, Locale locale) throws CRMException {
return updateArticle(entity.getClass(), entity.getId(), slug, title, summary, body, locale);
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Transactional
public Article updateArticle(Class<?> clazz, Long id, String slug, String title, String summary, String body, Locale locale) throws CRMException {
var lang = locale.toLanguageTag();
var article = articleService.getArticle(clazz, id, slug, locale);
if (article == null) {
throw new NotFoundElement("No such article");
}
var translation = article.translation;
if (Objects.equals(lang, article.entity.getOriginalLanguageTag())) {
article.entity.setTitle(title);
article.entity.setSummary(summary);
article.entity.setBody(body);
articleService.update(article.entity);
} else {
if (translation == null) { // No such translation
translation = new ArticleLang(article.entity, lang, title, summary, body);
} else {
translation.setTitle(title);
translation.setSummary(summary);
translation.setBody(body);
}
articleTranslationService.upsertLang(article.entity, translation);
}
return mapper.map(articleService.loadTranslated(article.entity.getId()));
}
@Override
public ClassPK getClassPk(Class<?> clazz) {
return classPkService.getClassPk(clazz);
}
@Override
public ClassPK getClassPk(String shortName) {
return classPkService.findByShortName(shortName);
}
@Override
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public synchronized ClassPK ensureClassPK(Class<?> clazz) {
return classPkService.getClassPk(clazz);
}
// @Override
// public Article getArticle(long id) {
// return articleRepository.findById(id).orElseThrow(() -> { throw new NotFoundElement("Record not found by id=" + id); });
// }
@Override
@Transactional(readOnly = true)
public ActivityPost getActivityPost(long id) {
var now = Instant.now();
var post = activityPostService.loadTranslated(id);
if (securityUtils.hasRole(UserRole.ADMINISTRATOR) || securityUtils.hasRole(UserRole.CONTENTMANAGER)) {
return mapper.map(post);
}
if ((post.getEntity().getPublishDate() != null && post.getEntity().getPublishDate().isAfter(now))
|| (post.getEntity().getExpirationDate() != null && post.getEntity().getExpirationDate().isBefore(now))) {
throw new NotFoundElement("Record not found by id=" + id);
}
return mapper.map(post);
}
/**
* Create and persist a new {@link ActivityPost}
*/
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Override
@Transactional
public ActivityPost createActivityPost(String title, String summary, String body, Instant publishDate, Instant expirationDate, RepositoryImage coverImage) {
// final ActivityPost newPost = new ActivityPost();
// newPost.setPublishDate(publishDate);
// newPost.setExpirationDate(expirationDate);
// newPost.setCoverImage(coverImage);
// return updatePostData(newPost, title, summary, body);
var source = new org.genesys.server.model.impl.ActivityPost();
source.setOriginalLanguageTag(getDefaultLocale().getLanguage());
source.setTitle(title);
source.setSummary(summary);
source.setBody(body);
source.setPublishDate(publishDate);
source.setExpirationDate(expirationDate);
source.setCoverImage(mapper.map(coverImage));
source = activityPostService.createFast(source);
return mapper.map(activityPostService.loadTranslated(source.getId()));
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Transactional
public ActivityPost updateActivityPost(long id, String title, String summary, String body) throws NotFoundElement {
var translatedPost = activityPostService.loadTranslated(id);
var post = translatedPost.entity;
if (Objects.equals(getDefaultLocale().getLanguage(), post.getOriginalLanguageTag())) {
post.setTitle(title);
post.setSummary(summary);
post.setBody(body);
post = activityPostService.update(post);
return mapper.map(activityPostService.loadTranslated(post.getId()));
} else { // No update
return mapper.map(translatedPost);
}
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Transactional
public ActivityPost updateActivityPost(long id, String title, String summary, String body, Instant publishDate, Instant expirationDate, RepositoryImage coverImage) throws NotFoundElement {
var translatedPost = activityPostService.loadTranslated(id);
var post = translatedPost.entity;
if (Objects.equals(getDefaultLocale().getLanguage(), post.getOriginalLanguageTag())) {
post.setTitle(title);
post.setSummary(summary);
post.setBody(body);
post.setPublishDate(publishDate);
post.setExpirationDate(expirationDate);
post.setCoverImage(mapper.map(coverImage));
post = activityPostService.update(post);
return mapper.map(activityPostService.loadTranslated(post.getId()));
} else { // No update
return mapper.map(translatedPost);
}
}
@Override
@Transactional(readOnly = true)
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
public Page<ActivityPost> listActivityPosts(ActivityPostFilter filter, Pageable pageable) throws SearchException {
return activityPostService.listFiltered(filter, pageable).map(mapper::map);
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Transactional
public void deleteActivityPost(ActivityPost activityPost) {
activityPostService.remove(activityPostService.get(activityPost.getId()));
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Transactional
public void deleteArticle(final Long id, final Integer version) {
articleService.deleteArticle(id, version);
}
@Override
public String processTemplate(String templateStr, Map<String, Object> root) {
final StringWriter swOut = new StringWriter();
processTemplate(templateStr, root, swOut);
if (LOG.isTraceEnabled()) {
LOG.trace(swOut.toString());
}
return swOut.toString();
}
@Override
public void processTemplate(String templateStr, Map<String, Object> root, Writer writer) {
final VelocityContext context = new VelocityContext();
for (final var entry : root.entrySet()) {
context.put(entry.getKey(), entry.getValue());
}
context.put("esc", new org.apache.velocity.tools.generic.EscapeTool());
/**
* Merge data and template
*/
velocityEngine.evaluate(context, writer, "log tag name", templateStr);
}
/**
* Load/get the CMS menu specified by the key
*
* @param key
* @return
*/
@Override
@Transactional(readOnly = true)
public Menu getMenu(String key) {
var menu = repoMenu.findByKey(key);
if (menu != null) {
menu.getItems().size();
}
return mapper.map(menu);
}
/**
* Ensure the CMS menu item exists in the menu
*
* @return
*/
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Transactional
public MenuItem ensureMenuItem(String menuKey, String url, String text) {
return this.ensureMenuItem(menuKey, new MenuItem(url, text));
}
/**
* Ensure the CMS menu item exists in the menu
*
* @return
*/
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Transactional
public MenuItem ensureMenuItem(String menuKey, MenuItem source) {
return mapper.map(menuItemService.ensureMenuItem(menuKey, mapper.map(source)));
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Transactional
public org.genesys.server.model.impl.MenuItem updateMenuItem(final long id, final MenuItem source) {
return menuItemService.update(mapper.map(source), menuItemService.get(id));
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Transactional
public MenuItem deleteMenuItem(final long id) {
return mapper.map(menuItemService.remove(menuItemService.get(id)));
}
@Override
@Transactional(readOnly = true)
public MenuItem getMenuItem(final long id) {
return mapper.map(menuItemService.loadTranslated(id));
}
/**
* Update the CMS menu specified by key with the provided structure
*
* @param key
* @param items
* @return
*/
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Transactional
public Menu updateMenu(String menuKey, List<MenuItem> items) {
// Menu menu = repoMenu.findByKey(menuKey);
// if (items.size() == 0) {
// if (menu != null) {
// LOG.info("Deleting CMS menu {}", menuKey);
// // Should delete all menu items
// repoMenu.delete(menu);
// } else {
// // NOOP
// LOG.info("Unknown CMS menu {}", menuKey);
// }
// return null;
// }
// if (menu == null) {
// // Create instance
// menu = new Menu();
// menu.setKey(menuKey);
// }
// // TODO Remove items missing from the List
// // TODO Add items in the list missing from the menu
// // TODO Persist.
// return menu;
throw new UnsupportedOperationException("Not implemented");
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
public void sanitizeAll() {
LOG.info("Sanitizing articles");
articleService.sanitizeAll();;
}
@Override
@Transactional
public Article createGlobalArticle(String slug, Locale locale, String title, String summary, String body, boolean isTemplate, Instant publishDate, Instant expirationDate) throws CRMException {
var article = articleService.getArticleBySlugAndLang(slug, locale.toLanguageTag());
if (article == null && !getDefaultLocale().getLanguage().equals(locale.toLanguageTag())) {
throw new CRMException("Global articles must first be created in primary language=" + getDefaultLocale().getLanguage());
}
if (article != null && article.entity.getOriginalLanguageTag().equals(locale.toLanguageTag())) {
throw new CRMException(String.format("Global article with slug='%s' and language='%s' already exists.", slug, locale.toLanguageTag()));
}
if (article == null) {
var source = new org.genesys.server.model.impl.Article();
source.setClassPk(ensureClassPK(Article.class));
source.setTargetId(0L);
source.setSlug(slug);
source.setTitle(title);
source.setSummary(summary);
source.setBody(body);
source.setTemplate(isTemplate);
source.setPublishDate(publishDate);
source.setExpirationDate(expirationDate);
source.setOriginalLanguageTag(locale.toLanguageTag());
// Create new
source = articleService.createFast(source);
return mapper.map(articleService.loadTranslated(source.getId()));
} else {
articleTranslationService.addLang(article.entity, new ArticleLang(article.entity, locale.toLanguageTag(), title, summary, body));
return mapper.map(articleService.loadTranslated(article.entity.getId()));
}
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER') or hasPermission(#article, 'ADMINISTRATION')")
public RepositoryFile uploadArticleFile(long articleId, final MultipartFile file) throws IOException, InvalidRepositoryPathException, InvalidRepositoryFileDataException {
if (file == null)
throw new InvalidRepositoryPathException("Nothing to upload. File must be provided.");
return articleService.uploadArticleFile(articleService.get(articleId), file);
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER') or hasPermission(#activityPost, 'ADMINISTRATION')")
public RepositoryFile uploadActivityPostFile(long activityPostId, final MultipartFile file) throws IOException, InvalidRepositoryPathException, InvalidRepositoryFileDataException {
if (file == null)
throw new InvalidRepositoryPathException("Nothing to upload. File must be provided.");
// Reload
return activityPostService.uploadActivityPostFile(activityPostService.get(activityPostId), file);
}
}