Commit eab9249c authored by Matija Obreza's avatar Matija Obreza
Browse files

Use external artifact org.genesys2.transifex-client

parent bae81f7b
/**
* 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.genesys2.server.servlet.controller.transifex;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Locale;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.genesys2.server.model.impl.Article;
import org.genesys2.server.service.ContentService;
import org.genesys2.server.servlet.controller.BaseController;
import org.genesys2.transifex.client.TransifexService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
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 org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
/**
* Transifex web hook listener.
* TODO Move other (non-hook) methods to ArticleController!
*
* @author matijaobreza
*
*/
@Controller
@RequestMapping(value = "/transifex")
public class TransifexAPIController extends BaseController {
......@@ -40,17 +48,8 @@ public class TransifexAPIController extends BaseController {
@Autowired
private ContentService contentService;
@Value("${transifex.project}")
private String projectSlug;
@Value("${transifex.username}")
private String trasifexUserName;
@Value("${transifex.password}")
private String transifexPassord;
@Value("${transifex.base.api.url}")
private String baseApiURL;
@Autowired
private TransifexService transifexService;
@Value("${transifex.content.template}")
private String contentTemplate;
......@@ -58,25 +57,15 @@ public class TransifexAPIController extends BaseController {
@Value("${transifex.min.translated}")
private int transifexMinTranslated;
private RestTemplate template = new RestTemplate();
@Value("${transifex.hook.key}")
private Object transifexHookKey;
@RequestMapping(value = "/deleteContent/{slug}")
public String deleteResource(@PathVariable String slug, Model model) {
// make authentication for Transifex
HttpHeaders headers = basicAuthentication();
Article article = contentService.getGlobalArticle(slug, getLocale());
if (resourceExists(article)) {
StringBuilder url = new StringBuilder();
url.append("project/");
url.append(projectSlug);
url.append("/resource/");
url.append("article-");
url.append(article.getSlug());
HttpEntity<String> request = new HttpEntity<>(headers);
ResponseEntity<String> response = template.exchange(baseApiURL + url, HttpMethod.DELETE, request, String.class);
if (response.getStatusCode().value() == 204) {
if (articleExists(article)) {
if (transifexService.deleteResource("article-".concat(article.getSlug()))) {
model.addAttribute("result", "deleted ok");
}
} else {
......@@ -94,17 +83,20 @@ public class TransifexAPIController extends BaseController {
*
* @see classpath:spring/spring-security.xml
*/
// FIXME make "magic" configurable
@RequestMapping(value = "/hook/40874cca86ca396169a5f4e6ebf6e4bf7199c4e7", method = RequestMethod.POST)
@RequestMapping(value = "/hook/{hookKey:.+}", method = RequestMethod.POST)
public @ResponseBody
String webHookHandle(@RequestParam(value = "project") String projectSlug, @RequestParam(value = "resource") String resource,
@RequestParam(value = "language") String language, @RequestParam(value = "translated") Integer translatedPercentage, Model model) {
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)) {
_logger.error("Invalid key provided for Transifex callback hook: " + hookKey);
throw new RuntimeException("I don't know you!");
}
// FIXME remove once confirmed to work
// TODO remove once confirmed to work
_logger.warn("project=" + projectSlug + " resource=" + resource + " lang=" + language + " translated=" + translatedPercentage);
// TODO may be revisit this
// currently we do this due to nature of Transifex hook
if (!resource.startsWith("article-")) {
_logger.warn("Ignoring Transifex'd hook for " + resource);
return "Ignored";
......@@ -141,23 +133,14 @@ public class TransifexAPIController extends BaseController {
private Article updateArticle(String slug, String lang) {
_logger.info("Fetching updated translation for article " + slug + " lang=" + lang);
// make authentication for Transifex
HttpHeaders headers = basicAuthentication();
StringBuilder url = new StringBuilder().append("project/").append(projectSlug).append("/resource/").append("article-").append(slug)
.append("/translation/").append(lang).append("/?file");
HttpEntity<String> request = new HttpEntity<>(headers);
ResponseEntity<String> response = template.exchange(baseApiURL + url, HttpMethod.GET, request, String.class);
Locale locale = new Locale(lang);
// TODO FIXME Check response status. Proceed only on HTTP OK response.
_logger.info("Response code=" + response.getStatusCode());
String resourceBody = transifexService.getTranslatedResource("article-".concat(slug), locale);
String title = response.getBody().split("<title>")[1].split("</title>")[0];
String body = response.getBody().split("<body>")[1].split("</body>")[0];
String title = resourceBody.split("<title>")[1].split("</title>")[0];
String body = resourceBody.split("<body>")[1].split("</body>")[0];
Locale locale = new Locale(lang);
// Extract article from database we need (correct locale + do not use
// default (EN) language)
Article article = contentService.getGlobalArticle(slug, locale, false);
......@@ -175,103 +158,35 @@ public class TransifexAPIController extends BaseController {
@RequestMapping(value = "/postContent/{slug:.+}")
public String postResourceToTransifex(@PathVariable String slug, Model model) throws IOException {
_logger.info("Posting to transifex slug=" + slug);
// Make the authentication for Transifex
HttpHeaders headers = basicAuthentication();
// Extract article from database we need
Article article = contentService.getGlobalArticle(slug, contentService.getDefaultLocale(), false);
if (!resourceExists(article)) {
// We will request like MULTIPART_FORM_DATA
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
if (!articleExists(article)) {
_logger.info("Posting to transifex slug=" + slug);
// This is template our xhtml
String content = String.format(contentTemplate, article.getTitle(), article.getBody());
// Create Multi value map with all necessary information for request
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
map.add("slug", "article-" + slug);
map.add("name", article.getTitle());
map.add("i18n_type", "XHTML");
File tempFile = File.createTempFile(slug, ".xhtml");
//default FileWriter support default encoding only
try (BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile))){
System.err.println(content);
IOUtils.write(content, writer);
writer.flush();
Resource resource = new FileSystemResource(tempFile);
map.add("content", resource);
// Create our request entity
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(map, headers);
StringBuilder url = new StringBuilder();
url.append("project/");
url.append(projectSlug);
url.append("/resources/");
// Send our post request(with .xhtml file) to Transifex
template.postForLocation(baseApiURL + url, request);
try {
transifexService.createXhtmlResource("article-".concat(article.getSlug()), article.getTitle(), content);
model.addAttribute("responseFromTransifex", "Resource added");
} catch (HttpClientErrorException e) {
_logger.error(e.getMessage());
_logger.error(e.getResponseBodyAsString());
model.addAttribute("responseFromTransifex", "fail");
} finally {
tempFile.delete();
}
} else {
// TODO Update contents!
// TODO Update contents on Transifex!
model.addAttribute("responseFromTransifex", "already exists");
}
model.addAttribute("title", article.getTitle());
model.addAttribute("article", article);
return "/content/article-edit";
}
// Transifex requires Basic HTTP authentication!
// This method makes authentication for Transifex
public HttpHeaders basicAuthentication() {
HttpHeaders headers = new HttpHeaders();
String trasifexCreds = trasifexUserName + ":" + transifexPassord;
byte[] transifexCredsBytes = trasifexCreds.getBytes();
byte[] base64CredsBytes = Base64.encodeBase64(transifexCredsBytes);
String base64Creds = new String(base64CredsBytes);
headers.add("Authorization", "Basic " + base64Creds);
return headers;
}
// This method verify if article already exists in Transifex
boolean resourceExists(Article resource) {
// make authentication for Transifex
HttpHeaders headers = basicAuthentication();
StringBuilder url = new StringBuilder();
url.append("project/");
url.append(projectSlug);
url.append("/resource/");
url.append("article-");
url.append(resource.getSlug());
ResponseEntity<String> response;
HttpEntity<String> request = new HttpEntity<>(headers);
try {
response = template.exchange(baseApiURL + url, HttpMethod.GET, request, String.class);
} catch (HttpClientErrorException e) {
return false;
}
return response.getStatusCode().value() == HttpStatus.OK.value();
boolean articleExists(Article article) {
return transifexService.resourceExists("article-".concat(article.getSlug()));
}
}
......@@ -16,13 +16,22 @@
package org.genesys2.spring.config;
import org.genesys2.transifex.client.TransifexService;
import org.genesys2.transifex.client.TransifexServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;
@Configuration
@Import({ SpringProperties.class, SpringCommonConfig.class, SpringAclConfig.class, SpringSchedulerConfig.class, SpringDataBaseConfig.class, SpringMailConfig.class,
SpringSecurityOauthConfig.class, SpringCacheConfig.class, ElasticsearchConfig.class })
@Import({ SpringProperties.class, SpringCommonConfig.class, SpringAclConfig.class, SpringSchedulerConfig.class, SpringDataBaseConfig.class,
SpringMailConfig.class, SpringSecurityOauthConfig.class, SpringCacheConfig.class, ElasticsearchConfig.class })
@ImportResource({ "classpath:/spring/spring-security.xml" })
public class ApplicationConfig {
@Bean
public TransifexService transifexService() {
return new TransifexServiceImpl();
}
}
/**
* 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.genesys2.transifex.client;
import java.io.IOException;
import java.util.Locale;
/**
* Transifex client
*
* @author matijaobreza
*
*/
public interface TransifexService {
boolean resourceExists(String slug);
boolean deleteResource(String slug);
String getTranslatedResource(String slug, Locale locale);
/**
* Create a new XHTML resource. Fails if resource exists.
*
* @param slug
* @param title
* @param content
* @throws IOException
*/
void createXhtmlResource(String slug, String title, String content) throws IOException;
/**
* Create or update XHTML resource
*
* @param resourceSlug
* @param resourceTitle
* @param content
* @throws IOException
*/
void updateXhtmlResource(String resourceSlug, String resourceTitle, String content) throws IOException;
}
/**
* 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.genesys2.transifex.client;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Locale;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
/**
* Java implementation of Transifex API 2
*
* @author alexandr
* @author matijaobreza
*
*/
@Service
public class TransifexServiceImpl implements TransifexService, InitializingBean {
private static final Logger LOG = Logger.getLogger(TransifexServiceImpl.class);
private static final String TRANSIFEX_API_URL = "https://www.transifex.com/api/2";
/*** Transifex project slug ***/
@Value("${transifex.project}")
private String projectSlug;
@Value("${transifex.username}")
private String trasifexUserName;
@Value("${transifex.password}")
private String transifexPassord;
private RestTemplate template = new RestTemplate();
// Transifex project url
private String transifexProjectUrl;
@Override
public void afterPropertiesSet() throws Exception {
transifexProjectUrl = TRANSIFEX_API_URL.concat("/project/").concat(projectSlug);
LOG.info("Transifex project URL: " + transifexProjectUrl);
}
// Transifex requires Basic HTTP authentication!
// This method makes authentication for Transifex
protected HttpHeaders basicAuthentication() {
HttpHeaders headers = new HttpHeaders();
String trasifexCreds = trasifexUserName + ":" + transifexPassord;
byte[] transifexCredsBytes = trasifexCreds.getBytes();
byte[] base64CredsBytes = Base64.encodeBase64(transifexCredsBytes);
String base64Creds = new String(base64CredsBytes);
headers.add("Authorization", "Basic " + base64Creds);
return headers;
}
@Override
public boolean resourceExists(String slug) {
if (LOG.isDebugEnabled()) {
LOG.debug("Checking for resource " + slug);
}
ResponseEntity<String> response;
HttpHeaders headers = basicAuthentication();
HttpEntity<String> request = new HttpEntity<>(headers);
try {
response = template.exchange(transifexProjectUrl + "/resource/{slug}", HttpMethod.GET, request, String.class, slug);
if (LOG.isDebugEnabled()) {
LOG.debug(response.getStatusCode() + " " + response.getBody());
}
} catch (HttpClientErrorException e) {
return false;
}
return response.getStatusCode().value() == HttpStatus.OK.value();
}
@Override
public boolean deleteResource(String slug) {
HttpHeaders headers = basicAuthentication();
HttpEntity<String> request = new HttpEntity<>(headers);
ResponseEntity<String> response = template.exchange(transifexProjectUrl + "/resource/{slug}", HttpMethod.DELETE, request, String.class, slug);
if (response.getStatusCode().value() == 204) {
return true;
}
return false;
}
@Override
public String getTranslatedResource(String slug, Locale locale) {
HttpHeaders headers = basicAuthentication();
HttpEntity<String> request = new HttpEntity<>(headers);
ResponseEntity<String> response = template.exchange(transifexProjectUrl + "/resource/{slug}/translation/{language}", HttpMethod.GET, request,
String.class, slug, locale.getLanguage());
// FIXME Check response status. Proceed only on HTTP OK response.
LOG.info("Response code=" + response.getStatusCode());
return response.getBody();
}
@Override
public void createXhtmlResource(String slug, String title, String content) throws IOException {
// Make the authentication for Transifex
HttpHeaders headers = basicAuthentication();
// We will request like MULTIPART_FORM_DATA
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
// Create Multi value map with all necessary information for request
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
map.add("slug", slug);
map.add("name", title);
map.add("i18n_type", "XHTML");
File tempFile = File.createTempFile(slug, ".xhtml");
// default FileWriter support default encoding only
try (BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile))) {
LOG.debug(content);
IOUtils.write(content, writer);
writer.flush();
Resource resource = new FileSystemResource(tempFile);
map.add("content", resource);
// Create our request entity
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(map, headers);
// Send our post request(with .xhtml file) to Transifex
ResponseEntity<Object> response = template.postForEntity(transifexProjectUrl + "/resources/", request, Object.class);
if (LOG.isDebugEnabled()) {
// 201 CREATED is returned by Transifex API 2
LOG.debug("Response: " + response.getStatusCode());
}
} catch (HttpClientErrorException e) {
LOG.error(e.getMessage());
LOG.error(e.getResponseBodyAsString());
throw e;
} finally {
tempFile.delete();
}
}
@Override
public void updateXhtmlResource(String slug, String title, String content) throws IOException {
if (!resourceExists(slug)) {
// POST the resource
createXhtmlResource(slug, title, content);
return;
}
LOG.info("Updating Transifex resource " + slug);
// Make the authentication for Transifex
HttpHeaders headers = basicAuthentication();
// We will request like MULTIPART_FORM_DATA
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
// Create Multi value map with all necessary information for request
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
// map.add("slug", slug);
// map.add("name", title);
// map.add("i18n_type", "XHTML");
File tempFile = File.createTempFile(slug, ".xhtml");
// default FileWriter support default encoding only
try (BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile))) {
LOG.debug(content);
IOUtils.write(content, writer);