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;
	}

}