TransifexAPIController.java

/**
 * Copyright 2014 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.mvc.transifex;

import java.io.IOException;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.genesys.server.component.aspect.AsAdmin;
import org.genesys.server.mvc.BaseController;
import org.genesys.server.service.ArticleService;
import org.genesys.server.service.ArticleTranslationService;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
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.ResponseBody;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * Transifex web hook listener.
 *
 * @author matijaobreza
 *
 */
@Controller
@RequestMapping(value = "/transifex")
public class TransifexAPIController extends BaseController {

	// Recognize slug-shortName-targetId pattern
	private static final Pattern scopedArticlePattern = Pattern.compile("^(.+)\\-(\\w+)\\-(\\d+)$", Pattern.CASE_INSENSITIVE);

	@Autowired
	private ArticleService articleService;

	@Autowired(required = false)
	private TransifexService transifexService;

	@Value("${transifex.min.translated}")
	private int transifexMinTranslated;

	@Value("${transifex.hook.key}")
	private Object transifexHookKey;

	@Autowired
	private ObjectMapper mapper;

	/**
	 * Note: the hook key value should be set in preferences
	 */
	@AsAdmin
	@RequestMapping(value = "/hook/{hookKey:.+}", method = RequestMethod.POST)
	public @ResponseBody String webHookHandle(@PathVariable("hookKey") String hookKey, @RequestParam(value = "project") String projectSlug,
			@RequestParam(value = "resource") String resource, @RequestParam(value = "language") String language,
			@RequestParam(value = "translated") Integer translatedPercentage, Model model) {

		if (!transifexHookKey.equals(hookKey)) {
			LOG.error("Invalid key provided for Transifex callback hook: {}", hookKey);
			throw new RuntimeException("I don't know you!");
		}

		if (!resource.startsWith("article-")) {
			LOG.warn("Ignoring Transifex'd hook for {}", resource);
			return "Ignored";
		}

		String slug = resource.split("-")[1];
		String classPkShortName = null;
		Long targetId = null;

		LOG.warn("Transifex'd article slug={}", slug);

		Matcher scopedMatcher = scopedArticlePattern.matcher(slug);

		if (scopedMatcher.matches()) {
			slug = scopedMatcher.group(1);
			classPkShortName = scopedMatcher.group(2);
			targetId = Long.parseLong(scopedMatcher.group(3));

			LOG.info("Found scoped article slug={} classPk={} target={}", slug, classPkShortName, targetId);
		} else {
			LOG.debug("Not a scopedArticle {}", slug);
		}

		if (translatedPercentage != null && translatedPercentage >= transifexMinTranslated) {
			// fetch updated resource from Transifex
			try {
				updateArticle(slug, classPkShortName, targetId, language);
			} catch (TransifexException e) {
				LOG.error(e.getMessage(), e);
				throw new RuntimeException("Server error");
			}
			return "Thanks!";
		} else {
			return "Not sufficiently translated";
		}
	}

	private void updateArticle(String slug, String classPkShortName, Long targetId, String language) throws TransifexException {
		if (transifexService == null) {
			LOG.error("translationService not available.");
			throw new TransifexException("transifex.com service not available", null);
		}

		LOG.info("Fetching updated translation for article {} lang={}", slug, language);

		Locale locale = new Locale(language);
		LOG.warn("Locale: {}", locale);

		String resourceBody = transifexService.getTranslatedResource("article-".concat(slug), locale, TranslationMode.ONLYTRANSLATED);
		String title = null;
		String body = null;
		String summary = null;

		try {
			JsonNode jsonObject = mapper.readTree(resourceBody);
			String content = jsonObject.get("content").asText();

			Document doc = Jsoup.parse(content);
			title = doc.title();
			LOG.info("Title: {}", title);

			if (content.contains("class=\"summary")) {
				// 1st <div class="summary">...
				summary = doc.body().child(0).html();
				LOG.info("Summary: {}", summary);

				// 2nd <div class="body">...
				body = doc.body().child(1).html();
				LOG.info("Body: {}", body);

			} else {
				// Old fashioned body-only approach
				doc.body().html();
			}
		} catch (IOException e) {
			LOG.error(e.getMessage(), e);
			throw new TransifexException("Oops", e);
		}

		// Extract article from database we need (correct locale + do not use
		// default (EN) language)
		ArticleTranslationService.TranslatedArticle article;
		if (targetId == null) {
			article = articleService.getGlobalArticle(slug, locale);
		} else {
			article = articleService.getArticleBySlugLangTargetIdClassPk(slug, language, targetId, classPkShortName);
		}
		// TODO Enable when tested
		// if (article == null) {
		// // No article for selected locale
		// article = contentService.createGlobalArticle(slug, locale, title,
		// body);
		// } else {
		// // Update article for locale
		// article = contentService.updateGlobalArticle(slug, locale, title,
		// body);
		// }

		LOG.info("Updated translation for article {} lang={}", slug, language);
	}
}