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