001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.scm.provider.svn.svnexe.command;
020
021import java.io.File;
022import java.io.FileOutputStream;
023import java.io.IOException;
024import java.io.PrintStream;
025import java.util.List;
026
027import org.apache.commons.lang3.StringUtils;
028import org.apache.maven.scm.provider.svn.repository.SvnScmProviderRepository;
029import org.apache.maven.scm.provider.svn.util.SvnUtil;
030import org.codehaus.plexus.util.Os;
031import org.codehaus.plexus.util.cli.CommandLineException;
032import org.codehaus.plexus.util.cli.CommandLineUtils;
033import org.codehaus.plexus.util.cli.Commandline;
034import org.codehaus.plexus.util.cli.StreamConsumer;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038/**
039 * Command line construction utility.
040 *
041 * @author Brett Porter
042 * @author Olivier Lamy
043 */
044public final class SvnCommandLineUtils {
045    private static final Logger LOGGER = LoggerFactory.getLogger(SvnCommandLineUtils.class);
046
047    private SvnCommandLineUtils() {}
048
049    public static void addTarget(Commandline cl, List<File> files) throws IOException {
050        if (files == null || files.isEmpty()) {
051            return;
052        }
053
054        StringBuilder sb = new StringBuilder();
055        String ls = System.getProperty("line.separator");
056        for (File f : files) {
057            sb.append(f.getPath().replace('\\', '/'));
058            sb.append(ls);
059        }
060
061        File targets = File.createTempFile("maven-scm-", "-targets");
062        try (PrintStream out = new PrintStream(new FileOutputStream(targets))) {
063            out.print(sb);
064        }
065
066        cl.createArg().setValue("--targets");
067        cl.createArg().setValue(targets.getAbsolutePath());
068
069        targets.deleteOnExit();
070    }
071
072    public static Commandline getBaseSvnCommandLine(
073            File workingDirectory, SvnScmProviderRepository repository, boolean interactive) {
074        Commandline cl = new Commandline();
075
076        cl.setExecutable("svn");
077        try {
078            cl.addSystemEnvironment();
079            cl.addEnvironment("LC_MESSAGES", "C");
080        } catch (Exception e) {
081            // Do nothing
082        }
083
084        if (workingDirectory != null) {
085            cl.setWorkingDirectory(workingDirectory.getAbsolutePath());
086        }
087
088        if (!StringUtils.isEmpty(System.getProperty("maven.scm.svn.config_directory"))) {
089            cl.createArg().setValue("--config-dir");
090            cl.createArg().setValue(System.getProperty("maven.scm.svn.config_directory"));
091        } else if (!StringUtils.isEmpty(SvnUtil.getSettings().getConfigDirectory())) {
092            cl.createArg().setValue("--config-dir");
093            cl.createArg().setValue(SvnUtil.getSettings().getConfigDirectory());
094        }
095
096        boolean hasAuthInfo = false;
097        if (repository != null && !StringUtils.isEmpty(repository.getUser())) {
098            hasAuthInfo = true;
099            cl.createArg().setValue("--username");
100            cl.createArg().setValue(repository.getUser());
101        }
102
103        if (repository != null && !StringUtils.isEmpty(repository.getPassword())) {
104            hasAuthInfo = true;
105            cl.createArg().setValue("--password");
106            cl.createArg().setValue(repository.getPassword());
107        }
108
109        // [by Lenik] don't overwrite existing auth cache by default.
110        if (hasAuthInfo && !SvnUtil.getSettings().isUseAuthCache()) {
111            cl.createArg().setValue("--no-auth-cache");
112        }
113
114        if (!interactive || SvnUtil.getSettings().isUseNonInteractive()) {
115            cl.createArg().setValue("--non-interactive");
116        }
117
118        if (SvnUtil.getSettings().isTrustServerCert()) {
119            cl.createArg().setValue("--trust-server-cert");
120        }
121
122        return cl;
123    }
124
125    public static int execute(Commandline cl, StreamConsumer consumer, CommandLineUtils.StringStreamConsumer stderr)
126            throws CommandLineException {
127        // SCM-482: force English resource bundle
128        cl.addEnvironment("LC_MESSAGES", "en");
129
130        int exitCode = CommandLineUtils.executeCommandLine(cl, consumer, stderr);
131
132        exitCode = checkIfCleanUpIsNeeded(exitCode, cl, consumer, stderr);
133
134        return exitCode;
135    }
136
137    public static int execute(
138            Commandline cl, CommandLineUtils.StringStreamConsumer stdout, CommandLineUtils.StringStreamConsumer stderr)
139            throws CommandLineException {
140        int exitCode = CommandLineUtils.executeCommandLine(cl, stdout, stderr);
141
142        exitCode = checkIfCleanUpIsNeeded(exitCode, cl, stdout, stderr);
143
144        return exitCode;
145    }
146
147    private static int checkIfCleanUpIsNeeded(
148            int exitCode, Commandline cl, StreamConsumer consumer, CommandLineUtils.StringStreamConsumer stderr)
149            throws CommandLineException {
150        if (exitCode != 0
151                && stderr.getOutput() != null
152                && stderr.getOutput().indexOf("'svn cleanup'") > 0
153                && stderr.getOutput().indexOf("'svn help cleanup'") > 0) {
154            if (LOGGER.isInfoEnabled()) {
155                LOGGER.info("Svn command failed due to some locks in working copy. We try to run a 'svn cleanup'.");
156            }
157
158            if (executeCleanUp(cl.getWorkingDirectory(), consumer, stderr) == 0) {
159                exitCode = CommandLineUtils.executeCommandLine(cl, consumer, stderr);
160            }
161        }
162        return exitCode;
163    }
164
165    public static int executeCleanUp(File workinDirectory, StreamConsumer stdout, StreamConsumer stderr)
166            throws CommandLineException {
167        Commandline cl = new Commandline();
168
169        cl.setExecutable("svn");
170
171        cl.setWorkingDirectory(workinDirectory.getAbsolutePath());
172
173        if (LOGGER.isInfoEnabled()) {
174            LOGGER.info("Executing: " + SvnCommandLineUtils.cryptPassword(cl));
175
176            if (Os.isFamily(Os.FAMILY_WINDOWS)) {
177                LOGGER.info("Working directory: " + cl.getWorkingDirectory().getAbsolutePath());
178            }
179        }
180
181        return CommandLineUtils.executeCommandLine(cl, stdout, stderr);
182    }
183
184    public static String cryptPassword(Commandline cl) {
185        String clString = cl.toString();
186
187        final String passwordOpt = "--password";
188        String quoteChar;
189        String escapedQuoteChar;
190        String cryptedPassword;
191
192        int pos = clString.indexOf(passwordOpt);
193
194        if (pos > 0) {
195            if (Os.isFamily(Os.FAMILY_WINDOWS)) {
196                quoteChar = "\"";
197                escapedQuoteChar = "\"\"";
198                cryptedPassword = "*****";
199            } else {
200                quoteChar = "'";
201                escapedQuoteChar = "'\"'\"'";
202                cryptedPassword = "'*****'";
203            }
204
205            // Move pointer after password option
206            pos += passwordOpt.length();
207
208            // Skip quote after password option
209            if (clString.substring(pos, pos + 1).equals(quoteChar)) {
210                pos++;
211            }
212
213            // Skip space after password option
214            pos++;
215
216            String beforePassword = clString.substring(0, pos);
217            String afterPassword = clString.substring(pos);
218
219            if (afterPassword.startsWith(quoteChar)) {
220                pos = 1;
221                while (afterPassword.indexOf(escapedQuoteChar, pos) != -1) {
222                    pos = afterPassword.indexOf(escapedQuoteChar, pos) + escapedQuoteChar.length();
223                }
224                afterPassword = afterPassword.substring(afterPassword.indexOf(quoteChar, pos) + quoteChar.length());
225            } else {
226                // We assume that the password arg ist not the last one on the arg list
227                afterPassword = afterPassword.substring(afterPassword.indexOf(' '));
228            }
229
230            clString = beforePassword + cryptedPassword + afterPassword;
231        }
232
233        return clString;
234    }
235}