package com.tinesoft.droidlinguist.server.tools; import java.io.IOException; import java.util.Arrays; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.servlet.http.HttpServletResponse; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; import com.tinesoft.droidlinguist.server.exception.TranslatedStringsBuilderException; import com.tinesoft.droidlinguist.server.json.translation.source.StringItemType; import com.tinesoft.droidlinguist.server.json.translation.target.TranslationFile; import com.tinesoft.droidlinguist.server.json.translation.target.TranslationResource; import com.tinesoft.droidlinguist.server.json.translation.target.TranslationState; import com.tinesoft.droidlinguist.server.json.translation.target.TranslationStringItem; import com.tinesoft.droidlinguist.server.json.translation.target.TranslationStringItemValue; /** * Component that builds the translated 'strings.xml' file for each target * language, and packages them into a single zip that will be streamed out in * the response to client. * * @author Tine Kondo * */ @Component public class TranslatedStringsBuilder { private static final Logger LOG = LoggerFactory.getLogger(TranslatedStringsBuilder.class); private static final String XMLNS_XLIFF = " xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\""; private static final String XMLNS_XLIFF_MARKER = "XMLNS_XLIFF"; private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; private static final String RESOURCES_TAG_START = "<resources " + XMLNS_XLIFF_MARKER + ">\n"; private static final String RESOURCES_TAG_END = "</resources>\n"; private static final String STRING_TAG_START = "\t<string name=\"%s\">"; private static final String STRING_TAG_END = "</string>\n"; private static final String STRING_ARRAY_TAG_START = "\t<string-array name=\"%s\">\n"; private static final String STRING_ARRAY_TAG_END = "\t</string-array>\n"; private static final String ITEM_QUANTITY_TAG_START = "\t\t<item quantity=\"%s\">"; private static final String ITEM_TAG_START = "\t\t<item>"; private static final String ITEM_TAG_END = "</item>\n"; private static final String PLURALS_TAG_START = "\t<plurals name=\"%s\">\n"; private static final String PLURALS_TAG_END = "\t</plurals>\n"; public void build(TranslationResource translation, HttpServletResponse response) { LOG.info("DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"); LOG.info(" >>> Building translated strings..."); if (translation == null) { LOG.error(" !!! Translation object is null"); throw new TranslatedStringsBuilderException(Arrays.asList("Invalid translation")); } // 1. validating parameters LOG.debug(" 1. Validating parameters..."); String name = StringUtils.isNotBlank(translation.getName()) ? sanitizeFileName(translation.getName()) : "strings.xml"; String sourceLang = translation.getSourceLang(); String state = translation.getState(); Map<String, TranslationFile> files = translation.getFiles(); if (!TranslationState.COMPLETED.equals(TranslationState.getbyCode(state))) { LOG.warn(" !!! Translation is not in 'COMPLETED'. State was '{}'", state); // throw new // TranslatedStringsBuilderException(Arrays.asList("Incomplete translation")); } if (files == null || files.isEmpty()) { LOG.warn(" !!! Translation is not valid. Target files list is empty"); // throw new // TranslatedStringsBuilderException(Arrays.asList("Incomplete translation")); } // 2 .preparing the response headers LOG.debug(" 2. Preparing the response headers..."); String filename = FilenameUtils.getBaseName(name) + ".zip"; response.setContentType("application/zip"); response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\""); response.setHeader(HttpHeaders.CONTENT_ENCODING, "utf-8"); // 3. creating and streaming the zip files with all translated xml files // in the response LOG.debug(" 3. creating and streaming the zip files with all translated xml files in the response..."); try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream());) { for (Map.Entry<String, TranslationFile> e : files.entrySet()) { String fileName = FilenameUtils.getBaseName(name) + "_" + e.getValue().getTargetLang() + ".xml"; addTranslationFileToZip(fileName, e.getValue(), zos); } zos.finish(); zos.flush(); } catch (IOException e) { LOG.error(" !!! Translation output zip could not be created", e); throw new TranslatedStringsBuilderException(Arrays.asList("Failed to create zip file for translated strings")); } LOG.info(" <<< Done building translated strings."); } public static String buildTranslationFileContent(TranslationFile file) { StringBuilder sb = new StringBuilder(); sb.append(XML_HEADER); sb.append(RESOURCES_TAG_START); boolean useXLIFF = false; for (TranslationStringItem tsi : file.getStrings()) { if (!tsi.isTranslatable())// non translatable contents should not be // included continue; StringItemType type = StringItemType.getByName(tsi.getType()); if (StringUtils.isNotBlank(tsi.getComment())) sb.append("\t<!-- ").append(tsi.getComment()).append("-->\n"); switch (type) { case STRING: if (!tsi.getValues().isEmpty()) { sb.append(String.format(STRING_TAG_START, tsi.getName())); sb.append(tsi.getValues().get(0).getText()); sb.append(STRING_TAG_END); } // TODO add warning tsi.getValues().isEmpty()? break; case STRING_ARRAY: if (!tsi.getValues().isEmpty()) { sb.append(String.format(STRING_ARRAY_TAG_START, tsi.getName())); for (TranslationStringItemValue tsiv : tsi.getValues()) { sb.append(ITEM_TAG_START); sb.append(tsiv.getText()); sb.append(ITEM_TAG_END); } sb.append(STRING_ARRAY_TAG_END); } // TODO add warning tsi.getValues().isEmpty()? break; case PLURALS: useXLIFF = true; if (!tsi.getValues().isEmpty()) { sb.append(String.format(PLURALS_TAG_START, tsi.getName())); for (TranslationStringItemValue tsiv : tsi.getValues()) { sb.append(String.format(ITEM_QUANTITY_TAG_START, tsiv.getQuantity())); sb.append(tsiv.getText()); sb.append(ITEM_TAG_END); } sb.append(PLURALS_TAG_END); } // TODO add warning tsi.getValues().isEmpty()? break; } } sb.append(RESOURCES_TAG_END); // add the xmlns:xliff if needed return sb.toString().replace(XMLNS_XLIFF_MARKER, useXLIFF ? XMLNS_XLIFF : ""); } private void addTranslationFileToZip(String name, TranslationFile file, ZipOutputStream zos) throws IOException { LOG.debug(" \t> Adding file '{}' to output zip", name); String fileContent = buildTranslationFileContent(file); ZipEntry entry = new ZipEntry(name); zos.putNextEntry(entry); zos.write(fileContent.getBytes("utf-8")); zos.closeEntry(); LOG.debug(" \t< File '{}' successfully added to output zip", name); } /** * Sanitizes a filename from certain chars.<br /> * * This method enforces the <code>forceSingleExtension</code> property and * then replaces all occurrences of \, /, |, :, ?, *, ", <, >, * control chars by _ (underscore). * * @param filename * a potentially 'malicious' filename * @return sanitized filename */ public static String sanitizeFileName(final String filename) { return filename != null ? filename.replaceAll("\\\\|/|\\||:|\\?|\\*|\"|<|>|\\p{Cntrl}", "_") : null; } }