CMSController.java
/*
* Copyright 2018 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.api.v1;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.model.ClassPK;
import org.genesys.filerepository.model.RepositoryFile;
import org.genesys.server.api.ApiBaseController;
import org.genesys.server.api.FilteredPage;
import org.genesys.server.api.Pagination;
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.exception.NotFoundElement;
import org.genesys.server.service.CRMException;
import org.genesys.server.service.ContentService;
import org.genesys.server.service.ShortFilterService.FilterInfo;
import org.genesys.server.service.filter.ActivityPostFilter;
import org.genesys.server.service.filter.ArticleFilter;
import org.genesys.server.exception.SearchException;
import org.genesys.server.service.worker.ShortFilterProcessor;
import org.genesys.transifex.client.TransifexException;
import org.genesys.transifex.client.TransifexService;
import org.genesys.transifex.client.TransifexService.TranslationMode;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springdoc.api.annotations.ParameterObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Sort;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.annotations.Api;
/**
* The Class CMSController.
*
* @author Maxym Borodenko
*/
@RestController("cmsApi1")
@PreAuthorize("isAuthenticated()")
@RequestMapping(CMSController.CONTROLLER_URL)
@Api(tags = { "cms" })
public class CMSController extends ApiBaseController {
/** The Constant CONTROLLER_URL. */
public static final String CONTROLLER_URL = ApiBaseController.APIv1_BASE + "/cms";
/** The Constant LOG. */
private static final Logger LOG = LoggerFactory.getLogger(CMSController.class);
@Autowired
private ContentService contentService;
/** The short filter service. */
@Autowired
private ShortFilterProcessor shortFilterProcessor;
@Autowired(required = false)
private TransifexService transifexService;
@Resource
private Set<String> supportedLocales;
private static final String defaultLanguage = "en";
@Autowired
private ObjectMapper objectMapper;
/**
* List of last 6 ActivityPost.
*/
@GetMapping(value = "/last-news")
public List<ActivityPost> getLastNews() {
return contentService.lastNews();
}
/**
* All activity posts.
*
* @param page the page
* @return the page
*/
@GetMapping(value = "/all-news")
public Page<ActivityPost> getAllNews(@ParameterObject final Pagination page) {
return contentService.allNews(page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, Sort.Direction.DESC, "publishDate"));
}
/**
* Gets the activity post.
*
* @param id the id of activity post
* @return the activity post
*/
@GetMapping(value = "/activity-post/{id:\\d+}")
public ActivityPost getActivityPost(@PathVariable("id") final long id) {
final ActivityPost activityPost = contentService.getActivityPost(id);
if (activityPost == null) {
throw new NotFoundElement("Activity post not found.");
}
return activityPost;
}
/**
* Gets the article
*
* @param slug the slug
* @param className the className of article
* @param targetId the targetId
* @param language the language
* @param useDefault the useDefault
* @return the article
*/
@RequestMapping(value = "/{slug:.+}/{clazz}/{targetId:\\d+}/{language}", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
public Article getArticle(@PathVariable(value = "clazz") final String className, @PathVariable("targetId") final long targetId,
@PathVariable(value = "slug") final String slug, @PathVariable(value = "language") final String language,
@RequestParam(name = "useDefault", required = false, defaultValue = "false") final boolean useDefault) throws ClassNotFoundException {
final Article article = contentService.getArticle(Class.forName(className), targetId, slug, new Locale(language), useDefault);
if (article == null) {
throw new NotFoundElement("Article not found.");
}
return article;
}
/**
* Gets the article
*
* @param slug the slug
* @param language the language
* @return the article
*/
@RequestMapping(value = "/article/{slug:.+}/{language}", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
public Article getArticleBySlugAndLang(@PathVariable(value = "slug") final String slug, @PathVariable(value = "language") final String language) {
final Article article = contentService.getArticleBySlugAndLang(slug, language);
if (article == null) {
throw new NotFoundElement("Article not found.");
}
return article;
}
/**
* Gets the global article
*
* @param slug the slug
* @param language the language
* @param useDefault the useDefault
* @return the global article
*/
@RequestMapping(value = "/global-article/{slug:.+}/{language}", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
public Article getGlobalArticle(@PathVariable(value = "slug") final String slug, @PathVariable(value = "language") final String language,
@RequestParam(name = "useDefault", required = false, defaultValue = "true") final boolean useDefault) {
final Article article = contentService.getGlobalArticle(slug, new Locale(language), useDefault);
if (article == null) {
throw new NotFoundElement("Article not found.");
}
return article;
}
/**
* List articles by filterCode or filter
*
* @param page the page
* @param filterCode short filter code
* @param filter the article filter
* @return the page
* @throws IOException
*/
@RequestMapping(value = "/list", method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE })
public FilteredPage<Article, ArticleFilter> listArticles(@RequestParam(name = "f", required = false) String filterCode, @ParameterObject final Pagination page,
@RequestBody(required = false) ArticleFilter filter) throws IOException, SearchException {
FilterInfo<ArticleFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, ArticleFilter.class);
return new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, contentService.listArticles(filterInfo.filter, page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, Sort.Direction.ASC, "id")));
}
/**
* Delete articles.
*
* @param entities the list of IDs and versions of articles
* @return list of operation responses
*/
@PostMapping(value = "/delete-articles", produces = { MediaType.APPLICATION_JSON_VALUE })
public List<OpResponse<Boolean>> deleteArticles(@RequestBody final List<IDandVersion> entities) {
return entities.stream().map(entity -> {
try {
contentService.deleteArticle(entity.id, entity.version);
return new OpResponse<>(true);
} catch (Throwable e) {
LOG.info("Error deleting article by id={}: {}", entity.id, e.getMessage());
return new OpResponse<Boolean>(e, false);
}
}).collect(Collectors.toList());
}
/**
* List activity posts by filterCode or filter
*
* @param page the page
* @param filterCode short filter code
* @param filter the activity post filter
* @return the page
* @throws IOException
* @throws SearchException
*/
@RequestMapping(value = "/activity-posts", method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE })
public FilteredPage<ActivityPost, ActivityPostFilter> listActivityPosts(@RequestParam(name = "f", required = false) String filterCode, @ParameterObject final Pagination page,
@RequestBody(required = false) ActivityPostFilter filter) throws IOException, SearchException {
FilterInfo<ActivityPostFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, ActivityPostFilter.class);
return new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, contentService.listActivityPosts(filterInfo.filter, page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, Sort.Direction.ASC, "id")));
}
/**
* Create the activity post.
*
* @param post the activity post
* @return the activity post
*/
@RequestMapping(value = "/create-post", method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE })
public ActivityPost createActivityPost(@RequestBody final ActivityPost post) {
return contentService.createActivityPost(post.getTitle(), post.getSummary(), post.getBody(), post.getPublishDate(), post.getExpirationDate(), post.getCoverImage());
}
/**
* Update the article.
*
* @param article the article
* @param className the className of article
* @return the updated article
*/
@RequestMapping(value = "/update-article", method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE })
public Article updateArticle(@RequestParam(name = "clazz", required = false) String className, @RequestBody final Article article) throws NotFoundElement, ClassNotFoundException, CRMException {
if (className == null) {
return contentService.updateArticle(article.getId(), article.getSlug(), article.getTitle(), article.getSummary(), article.getBody());
} else {
return contentService.updateArticle(Class.forName(className), article.getTargetId(), article.getSlug(), article.getTitle(), article.getSummary(), article.getBody(), new Locale(article.getLang()));
}
}
/**
* Create the article.
*
* @param article the article
* @return the updated article
*/
@RequestMapping(value = "/create-article", method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE })
public Article createGlobalArticle(@RequestBody final Article article) throws CRMException {
return contentService.createGlobalArticle(article.getSlug(), new Locale(article.getLang()), article.getTitle(), article.getSummary(), article.getBody(), article.isTemplate(), article.getPublishDate(), article.getExpirationDate());
}
/**
* Update the global article.
*
* @param article the article
* @return the updated global article
*/
@RequestMapping(value = "/update-global-article", method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE })
public Article updateGlobalArticle(@RequestBody final Article article) throws CRMException {
return contentService.updateGlobalArticle(article.getSlug(), new Locale(article.getLang()), article.getTitle(), article.getSummary(), article.getBody(), article.getPublishDate(), article.getExpirationDate());
}
/**
* Update the activity post.
*
* @param post the activity post
* @return the updated activity post
*/
@RequestMapping(value = "/update-activity-post", method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE })
public ActivityPost updateActivityPost(@RequestBody final ActivityPost post) throws NotFoundElement {
return contentService.updateActivityPost(post.getId(), post.getTitle(), post.getSummary(), post.getBody(), post.getPublishDate(), post.getExpirationDate(), post.getCoverImage());
}
/**
* Delete the activity post.
*
* @param id the id of activity post
* @return true if deleted
*/
@RequestMapping(value = "delete-post/{id:\\d+}", method = { RequestMethod.DELETE, RequestMethod.POST }, produces = { MediaType.APPLICATION_JSON_VALUE })
public Boolean deleteActivityPost(@PathVariable("id") final long id) throws NotFoundElement {
contentService.deleteActivityPost(contentService.getActivityPost(id));
return true;
}
/**
* Uploads a file to Article folder
*
* @param id the id of Article
* @param file the file to upload
* @return persisted file
* @throws Exception
*/
@PostMapping(value = "/article/{id:\\d+}/upload", produces = { MediaType.APPLICATION_JSON_VALUE })
public RepositoryFile uploadArticleFile(@PathVariable("id") final long id, @RequestPart(name = "file") final MultipartFile file)
throws Exception {
return contentService.uploadArticleFile(id, file);
}
/**
* Uploads a file to ActivityPost folder
*
* @param id the id of ActivityPost
* @param file the file to upload
* @return persisted file
* @throws Exception
*/
@PostMapping(value = "/activity-post/{id:\\d+}/upload", produces = { MediaType.APPLICATION_JSON_VALUE })
public RepositoryFile uploadActivityPostFile(@PathVariable("id") final long id, @RequestPart(name = "file") final MultipartFile file)
throws Exception {
return contentService.uploadActivityPostFile(id, file);
}
/**
* Gets the default locale.
*
* @return the default locale
*/
@RequestMapping(value = "/locale", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
public Locale getDefaultLocale() {
return contentService.getDefaultLocale();
}
/**
* Process template.
*
* @param body the body
* @param root the map
* @return the processed template in string
*/
@RequestMapping(value = "/process-template", method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE })
public String processTemplate(@RequestParam(name = "body", required = true) final String body, @RequestBody final Map<String, Object> root) {
return contentService.processTemplate(body, root);
}
/**
* Gets the CMS menu.
*
* @param menuKey the key of menu
*
* @return the CMS menu
*/
@RequestMapping(value = "/menu/{menuKey}", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
public Menu getMenu(@PathVariable(value = "menuKey") final String menuKey) {
final Menu menu = contentService.getMenu(menuKey);
if (menu == null) {
throw new NotFoundElement("Menu not found by key=" + menuKey);
}
return menu;
}
/**
* Update the CMS menu.
*
* @param menuKey the key of menu
* @param items the menu items
* @return the updated CMS menu
*/
@RequestMapping(value = "/update-menu/{menuKey}", method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE })
public Menu updateMenu(@PathVariable(value = "menuKey") final String menuKey, @RequestBody final List<MenuItem> items) {
return contentService.updateMenu(menuKey, items);
}
/**
* Ensure menu item.
*
* @param menuKey the key of menu
* @param source the menu item to be created
* @return the menu item
*/
@RequestMapping(value = "/ensure-menu-item/{menuKey}", method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE })
public MenuItem ensureMenuItem(@PathVariable(value = "menuKey") final String menuKey, @RequestBody final MenuItem source) {
return contentService.ensureMenuItem(menuKey, source);
}
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@PutMapping(value = "/menu-item/{id}", produces = { MediaType.APPLICATION_JSON_VALUE })
public MenuItem updateMenuItem(@PathVariable(value = "id") final long id, @RequestBody final MenuItem source) {
return contentService.getMenuItem(contentService.updateMenuItem(id, source).getId());
}
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@DeleteMapping(value = "/menu-item/{id}", produces = { MediaType.APPLICATION_JSON_VALUE })
public MenuItem deleteMenuItem(@PathVariable(value = "id") final long id) {
return contentService.deleteMenuItem(id);
}
@PostMapping(value = "/transifex")
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
public Article postToTransifex(@RequestParam("slug") String slug,
@RequestParam(value = "targetId", required = false) Long targetId, @RequestParam("classPkShortName") String classPkShortName) throws IOException {
if (transifexService == null) {
throw new NotFoundElement("translationService not enabled");
}
Article article;
String resourceName;
if (targetId != null) {
article = contentService.getArticleBySlugLangTargetIdClassPk(slug, LocaleContextHolder.getLocale().toLanguageTag(), targetId, classPkShortName);
resourceName = "article-" + slug + "-" + classPkShortName + "-" + targetId;
} else {
article = contentService.getGlobalArticle(slug, LocaleContextHolder.getLocale());
resourceName = "article-" + slug;
}
String body = String.format("<div class=\"summary\">%s</div><div class=\"body\">%s</div>", article.getSummary(), article.getBody());
if (transifexService.resourceExists(resourceName)) {
transifexService.updateXhtmlResource(resourceName, article.getTitle(), body);
} else {
transifexService.createXhtmlResource(resourceName, article.getTitle(), body);
}
return article;
}
@DeleteMapping(value = "/transifex")
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
public Boolean deleteFromTransifex(@RequestParam("slug") String slug,
@RequestParam(value = "targetId", required = false) Long targetId, @RequestParam("classPkShortName") String classPkShortName) throws TransifexException {
if (transifexService == null) {
throw new NotFoundElement("translationService not enabled");
}
String resourceName;
if (targetId != null) {
resourceName = "article-" + slug + "-" + classPkShortName + "-" + targetId;
} else {
resourceName = "article-" + slug;
}
return transifexService.deleteResource(resourceName);
}
/**
* Fetch all from Transifex and store
*
* @param slug - article slug
* @return map with results of fetching
* @throws Exception
*/
@PostMapping(value = "/transifex/fetchAll")
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
public Map<String, String> fetchAllFromTransifex(@RequestParam("slug") String slug, @RequestParam(value = "targetId", required = false) Long targetId,
@RequestParam(value = "classPkShortName", required = false) String classPkShortName, @RequestParam(name = "template", defaultValue = "false") Boolean template) throws Exception {
if (transifexService == null) {
throw new NotFoundElement("translationService not enabled");
}
String resourceName;
if (targetId != null) {
resourceName = "article-" + slug + "-" + classPkShortName + "-" + targetId;
} else {
resourceName = "article-" + slug;
}
Map<String, String> responses = new HashMap<>(20);
for (String lang : supportedLocales) {
if (defaultLanguage.equalsIgnoreCase(lang)) {
continue;
}
Locale locale = Locale.forLanguageTag(lang);
LOG.info("Fetching article {} translation for {}", resourceName, locale);
String translatedResource;
try {
translatedResource = transifexService.getTranslatedResource(resourceName, locale, TranslationMode.ONLYTRANSLATED);
} catch (TransifexException e) {
LOG.warn(e.getMessage(), e);
if (e.getCause() != null) {
responses.put(lang, e.getLocalizedMessage() + ": " + e.getCause().getLocalizedMessage());
} else {
responses.put(lang, e.getLocalizedMessage());
}
// throw new Exception(e.getMessage(), e);
continue;
}
String title;
String body;
String summary = null;
try {
JsonNode jsonObject = objectMapper.readTree(translatedResource);
String content = jsonObject.get("content").asText();
Document doc = Jsoup.parse(content);
title = doc.title();
if (content.contains("class=\"summary")) {
// 1st <div class="summary">...
summary = doc.body().child(0).html();
// 2nd <div class="body">...
body = doc.body().child(1).html();
} else {
// Old fashioned body-only approach
body = doc.body().html();
}
if (targetId != null && StringUtils.isNotBlank(classPkShortName)) {
ClassPK classPk = contentService.getClassPk(classPkShortName);
contentService.updateArticle(Class.forName(classPk.getClassname()), targetId, slug, title, summary, body, new Locale(lang));
responses.put(lang, "article.translations-updated");
} else if (targetId == null && StringUtils.isBlank(classPkShortName)) {
contentService.updateGlobalArticle(slug, locale, title, summary, body);
responses.put(lang, "article.translations-updated");
} else {
responses.put(lang, "Error updating local content");
}
} catch (IOException e) {
LOG.warn(e.getMessage(), e);
responses.put(lang, e.getMessage());
}
}
return responses;
}
}