diff --git a/client/zanata-client-ant-properties/pom.xml b/client/zanata-client-ant-properties/pom.xml index 6508e48319..15a1e0f591 100644 --- a/client/zanata-client-ant-properties/pom.xml +++ b/client/zanata-client-ant-properties/pom.xml @@ -47,6 +47,11 @@ zanata-adapter-properties ${project.version} + + org.zanata + zanata-client-commands + ${project.version} + org.zanata zanata-rest-client diff --git a/client/zanata-client-commands/pom.xml b/client/zanata-client-commands/pom.xml index 567c3ab529..4a634b6bd2 100644 --- a/client/zanata-client-commands/pom.xml +++ b/client/zanata-client-commands/pom.xml @@ -71,7 +71,11 @@ commons-io commons-io - 1.4 + 2.0.1 + + org.fedorahosted.openprops + openprops + diff --git a/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/GettextDirStrategy.java b/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/GettextDirStrategy.java new file mode 100644 index 0000000000..99d8bfe9f6 --- /dev/null +++ b/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/GettextDirStrategy.java @@ -0,0 +1,130 @@ +/* + * Copyright 2011, Red Hat, Inc. and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.zanata.client.commands.push; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.xml.sax.InputSource; +import org.zanata.adapter.po.PoReader2; +import org.zanata.client.commands.StringUtil; +import org.zanata.client.commands.gettext.PublicanUtil; +import org.zanata.client.commands.push.PushCommand.TranslationResourcesVisitor; +import org.zanata.client.config.LocaleMapping; +import org.zanata.common.LocaleId; +import org.zanata.rest.StringSet; +import org.zanata.rest.dto.resource.Resource; +import org.zanata.rest.dto.resource.TranslationsResource; + +class GettextDirStrategy implements PushStrategy +{ + StringSet extensions = new StringSet("comment;gettext"); + PoReader2 poReader = new PoReader2(); + List locales; + private PushOptions opts; + + @Override + public void setPushOptions(PushOptions opts) + { + this.opts = opts; + } + + @Override + public StringSet getExtensions() + { + return extensions; + } + + @Override + public Set findDocNames(File srcDir) throws IOException + { + Set localDocNames = new HashSet(); + // populate localDocNames by looking in pot directory + String[] srcFiles = PublicanUtil.findPotFiles(srcDir); + for (String potName : srcFiles) + { + String docName = StringUtil.removeFileExtension(potName, ".pot"); + localDocNames.add(docName); + } + return localDocNames; + } + + @Override + public Resource loadSrcDoc(File sourceDir, String docName) + { + File srcFile = new File(sourceDir, docName + ".pot"); + InputSource srcInputSource = new InputSource(srcFile.toURI().toString()); + // load 'srcDoc' from pot/${docID}.pot + return poReader.extractTemplate(srcInputSource, new LocaleId(opts.getSourceLang()), docName); + } + + private List findLocales() + { + if (locales != null) + return locales; + if (opts.getPushTrans()) + { + if (opts.getLocales() != null) + { + locales = PublicanUtil.findLocales(opts.getTransDir(), opts.getLocales()); + if (locales.size() == 0) + { + PushCommand.log.warn("option 'pushTrans' is set, but none of the configured locale directories was found (check zanata.xml)"); + } + } + else + { + locales = PublicanUtil.findLocales(opts.getTransDir()); + if (locales.size() == 0) + { + PushCommand.log.warn("option 'pushTrans' is set, but no locale directories were found"); + } + else + { + PushCommand.log.info("option 'pushTrans' is set, but no locales specified in configuration: importing " + locales.size() + " directories"); + } + } + } + return locales; + } + + @Override + public void visitTranslationResources(String docUri, String docName, Resource srcDoc, TranslationResourcesVisitor callback) + { + for (LocaleMapping locale : findLocales()) + { + File localeDir = new File(opts.getTransDir(), locale.getLocalLocale()); + File transFile = new File(localeDir, docName + ".po"); + if (transFile.exists()) + { + InputSource inputSource = new InputSource(transFile.toURI().toString()); + inputSource.setEncoding("utf8"); + TranslationsResource targetDoc = poReader.extractTarget(inputSource, srcDoc); + callback.visit(locale, targetDoc); + } + } + } + +} \ No newline at end of file diff --git a/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/PushCommand.java b/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/PushCommand.java new file mode 100644 index 0000000000..63315de728 --- /dev/null +++ b/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/PushCommand.java @@ -0,0 +1,235 @@ +package org.zanata.client.commands.push; + +import java.io.Console; +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; + +import org.jboss.resteasy.client.ClientResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.zanata.client.commands.ConfigurableProjectCommand; +import org.zanata.client.commands.OptionsUtil; +import org.zanata.client.config.LocaleMapping; +import org.zanata.common.LocaleId; +import org.zanata.rest.RestUtil; +import org.zanata.rest.StringSet; +import org.zanata.rest.client.ClientUtility; +import org.zanata.rest.client.ITranslationResources; +import org.zanata.rest.client.ZanataProxyFactory; +import org.zanata.rest.dto.resource.Resource; +import org.zanata.rest.dto.resource.ResourceMeta; +import org.zanata.rest.dto.resource.TranslationsResource; + +/** + * @author Sean Flanigan sflaniga@redhat.com + * + */ +public class PushCommand extends ConfigurableProjectCommand +{ + static final Logger log = LoggerFactory.getLogger(PushCommand.class); + + private static final Map strategies = new HashMap(); + + static interface TranslationResourcesVisitor + { + void visit(LocaleMapping locale, TranslationsResource targetDoc); + } + + { + // strategies.put("properties", new PropertiesStrategy()); + strategies.put("gettextDir", new GettextDirStrategy()); + } + + Marshaller m = null; + + private final PushOptions opts; + private final ITranslationResources translationResources; + private final URI uri; + + public PushCommand(PushOptions opts, ZanataProxyFactory factory, ITranslationResources translationResources, URI uri) + { + super(opts, factory); + this.opts = opts; + this.translationResources = translationResources; + this.uri = uri; + } + + private PushCommand(PushOptions opts, ZanataProxyFactory factory) + { + this(opts, factory, factory.getTranslationResources(opts.getProj(), opts.getProjectVersion()), factory.getTranslationResourcesURI(opts.getProj(), opts.getProjectVersion())); + } + + public PushCommand(PushOptions opts) + { + this(opts, OptionsUtil.createRequestFactory(opts)); + } + + @Override + public void run() throws Exception + { + log.info("Server: {}", opts.getUrl()); + log.info("Project: {}", opts.getProj()); + log.info("Version: {}", opts.getProjectVersion()); + log.info("Username: {}", opts.getUsername()); + log.info("Project type: {}", opts.getProjectType()); + log.info("Source language: {}", opts.getSourceLang()); + log.info("Copy previous translations: {}", opts.getCopyTrans()); + log.info("Merge type: {}", opts.getMergeType()); + if (opts.getPushTrans()) + { + log.info("Pushing source and target documents"); + } + else + { + log.info("Pushing source documents only"); + } + log.info("Source directory (originals): {}", opts.getSourceDir()); + if (opts.getPushTrans()) + { + log.info("Target base directory (translations): {}", opts.getTransDir()); + } + File sourceDir = opts.getSourceDir(); + + if (!sourceDir.exists()) + { + throw new RuntimeException("directory '" + sourceDir + "' does not exist - check sourceDir option"); + } + + if (opts.getPushTrans()) + { + log.warn("pushTrans option is set: existing translations on server may be overwritten/deleted"); + confirmWithUser("This will overwrite/delete any existing documents AND TRANSLATIONS on the server.\n"); + } + else + { + confirmWithUser("This will overwrite/delete any existing documents on the server.\n"); + } + + PushStrategy strat = strategies.get(opts.getProjectType()); + if (strat == null) + { + throw new RuntimeException("unknown project type: " + opts.getProjectType()); + } + + JAXBContext jc = null; + if (opts.isDebugSet()) // || opts.getValidate()) + { + jc = JAXBContext.newInstance(Resource.class, TranslationsResource.class); + } + if (opts.isDebugSet()) + { + m = jc.createMarshaller(); + m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + } + + // NB we don't load all the docs into a HashMap, because that would waste + // memory + Set localDocNames = strat.findDocNames(sourceDir); + deleteObsoleteDocsFromServer(localDocNames); + + for (String docName : localDocNames) + { + final String docUri = RestUtil.convertToDocumentURIId(docName); + final Resource srcDoc = strat.loadSrcDoc(sourceDir, docName); + debug(srcDoc); + // if (opts.getValidate()) + // { + // JaxbUtil.validateXml(srcDoc, jc); + // } + + final StringSet extensions = strat.getExtensions(); + log.info("pushing source document [name={}] to server", srcDoc.getName()); + boolean copyTrans = opts.getCopyTrans(); + ClientResponse putResponse = translationResources.putResource(docUri, srcDoc, extensions, copyTrans ); + ClientUtility.checkResult(putResponse, uri); + + if (opts.getPushTrans()) + { + strat.visitTranslationResources(docUri, docName, srcDoc, new TranslationResourcesVisitor() + { + @Override + public void visit(LocaleMapping locale, TranslationsResource targetDoc) + { + debug(targetDoc); + // if (opts.getValidate()) + // { + // JaxbUtil.validateXml(targetDoc, jc); + // } + log.info("pushing target document [name={} client-locale={}] to server [locale={}]", new Object[] { srcDoc.getName(), locale.getLocalLocale(), locale.getLocale() }); + ClientResponse putTransResponse = translationResources.putTranslations(docUri, new LocaleId(locale.getLocale()), targetDoc, extensions, opts.getMergeType()); + ClientUtility.checkResult(putTransResponse, uri); + } + }); + } + } + } + + protected void deleteObsoleteDocsFromServer(Set localDocNames) + { + ClientResponse> getResponse = translationResources.get(null); + ClientUtility.checkResult(getResponse, uri); + List remoteDocList = getResponse.getEntity(); + for (ResourceMeta doc : remoteDocList) + { + // NB ResourceMeta.name = HDocument.docId + String docName = doc.getName(); + String docUri = RestUtil.convertToDocumentURIId(docName); + if (!localDocNames.contains(docName)) + { + log.info("deleting resource {} from server", docName); + ClientResponse deleteResponse = translationResources.deleteResource(docUri); + ClientUtility.checkResult(deleteResponse, uri); + } + } + } + + private void confirmWithUser(String message) throws IOException + { + if (opts.isInteractiveMode()) + { + Console console = System.console(); + if (console == null) + throw new RuntimeException("console not available: please run Maven from a console, or use batch mode (mvn -B)"); + console.printf(message + "\nAre you sure (y/n)? "); + expectYes(console); + } + } + + protected static void expectYes(Console console) throws IOException + { + String line = console.readLine(); + if (line == null) + throw new IOException("console stream closed"); + if (!line.toLowerCase().equals("y") && !line.toLowerCase().equals("yes")) + throw new RuntimeException("operation aborted by user"); + } + + protected void debug(Object jaxbElement) + { + try + { + if (opts.isDebugSet()) + { + StringWriter writer = new StringWriter(); + m.marshal(jaxbElement, writer); + log.debug("{}", writer); + } + } + catch (JAXBException e) + { + log.debug(e.toString(), e); + } + } + +} diff --git a/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/PushOptions.java b/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/PushOptions.java new file mode 100644 index 0000000000..5f6c89342d --- /dev/null +++ b/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/PushOptions.java @@ -0,0 +1,17 @@ +package org.zanata.client.commands.push; + +import java.io.File; + +import org.zanata.client.commands.ConfigurableProjectOptions; + +public interface PushOptions extends ConfigurableProjectOptions +{ + public String getSourceLang(); + public File getSourceDir(); + public File getTransDir(); + public String getProjectType(); + public boolean getPushTrans(); + public boolean getCopyTrans(); + public String getMergeType(); +} + diff --git a/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/PushStrategy.java b/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/PushStrategy.java new file mode 100644 index 0000000000..ea821f40aa --- /dev/null +++ b/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/PushStrategy.java @@ -0,0 +1,39 @@ +/* + * Copyright 2011, Red Hat, Inc. and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package org.zanata.client.commands.push; + +import java.io.File; +import java.io.IOException; +import java.util.Set; + +import org.zanata.client.commands.push.PushCommand.TranslationResourcesVisitor; +import org.zanata.rest.StringSet; +import org.zanata.rest.dto.resource.Resource; + +interface PushStrategy +{ + void setPushOptions(PushOptions opts); + StringSet getExtensions(); + Set findDocNames(File srcDir) throws IOException; + Resource loadSrcDoc(File sourceDir, String docName) throws IOException; + void visitTranslationResources(String docUri, String docName, Resource srcDoc, TranslationResourcesVisitor visitor); +} \ No newline at end of file diff --git a/client/zanata-maven-plugin/src/main/java/org/zanata/maven/PushMojo.java b/client/zanata-maven-plugin/src/main/java/org/zanata/maven/PushMojo.java new file mode 100644 index 0000000000..ad7b3dbdfd --- /dev/null +++ b/client/zanata-maven-plugin/src/main/java/org/zanata/maven/PushMojo.java @@ -0,0 +1,123 @@ +package org.zanata.maven; + +import java.io.File; + +import org.zanata.client.commands.push.PushCommand; +import org.zanata.client.commands.push.PushOptions; + +/** + * Pushes source text to a Zanata project version so that it can be translated. + * + * @goal push + * @requiresProject true + * @author Sean Flanigan + */ +public class PushMojo extends ConfigurableProjectMojo implements PushOptions +{ + + public PushMojo() throws Exception + { + super(); + } + + @Override + public PushCommand initCommand() + { + return new PushCommand(this); + } + + /** + * Base directory for source-language files + * + * @parameter expression="${zanata.sourceDir}" default-value="." + */ + private File sourceDir; + + /** + * Base directory for target-language files (translations) + * + * @parameter expression="${zanata.transDir}" default-value="." + */ + private File transDir; + + /** + * Type of project ("properties" is the only supported type at present) + * + * @parameter expression="${zanata.projectType}" + * @required + */ + private String projectType; + + /** + * Language of source documents + * + * @parameter expression="${zanata.sourceLang}" default-value="en-US" + */ + private String sourceLang = "en-US"; + + /** + * Push translations from local files to the server (merge or import: see + * mergeType) + * + * @parameter expression="${zanata.pushTrans}" + */ + private boolean pushTrans; + + /** + * Whether the server should copy latest translations from equivalent + * messages/documents in the database (only applies to new documents) + * + * @parameter expression="${zanata.copyTrans}" default-value="true" + */ + private boolean copyTrans; + + /** + * Merge type: "auto" (default) or "import" (DANGER!). + * + * @parameter expression="${zanata.merge}" default-value="auto" + */ + private String merge; + + @Override + public File getSourceDir() + { + return sourceDir; + } + + @Override + public File getTransDir() + { + return transDir; + } + + @Override + public String getProjectType() + { + return projectType; + } + + @Override + public String getSourceLang() + { + return sourceLang; + } + + @Override + public boolean getPushTrans() + { + return pushTrans; + } + + @Override + public boolean getCopyTrans() + { + return copyTrans; + } + + @Override + public String getMergeType() + { + return merge; + } + +}