001package org.apache.maven.scm.provider.perforce.command.checkout;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 * http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import org.apache.maven.scm.ScmException;
023import org.apache.maven.scm.ScmFileSet;
024import org.apache.maven.scm.ScmVersion;
025import org.apache.maven.scm.command.checkout.AbstractCheckOutCommand;
026import org.apache.maven.scm.command.checkout.CheckOutScmResult;
027import org.apache.maven.scm.provider.ScmProviderRepository;
028import org.apache.maven.scm.provider.perforce.PerforceScmProvider;
029import org.apache.maven.scm.provider.perforce.command.PerforceCommand;
030import org.apache.maven.scm.provider.perforce.repository.PerforceScmProviderRepository;
031import org.codehaus.plexus.util.IOUtil;
032import org.codehaus.plexus.util.StringUtils;
033import org.codehaus.plexus.util.cli.CommandLineException;
034import org.codehaus.plexus.util.cli.CommandLineUtils;
035import org.codehaus.plexus.util.cli.Commandline;
036
037import java.io.BufferedReader;
038import java.io.ByteArrayInputStream;
039import java.io.File;
040import java.io.IOException;
041import java.io.InputStreamReader;
042import java.util.regex.Matcher;
043import java.util.regex.Pattern;
044
045/**
046 * @author Mike Perham
047 *
048 */
049public class PerforceCheckOutCommand
050    extends AbstractCheckOutCommand
051    implements PerforceCommand
052{
053    private String actualLocation;
054
055    /**
056     * Check out the depot code at <code>repo.getPath()</code> into the target
057     * directory at <code>files.getBasedir</code>. Perforce does not support
058     * arbitrary checkout of versioned source so we need to set up a well-known
059     * clientspec which will hold the required info.
060     * <p/>
061     * 1) A clientspec will be created or updated which holds a temporary
062     * mapping from the repo path to the target directory.
063     * 2) This clientspec is sync'd to pull all the files onto the client
064     * <p/>
065     * {@inheritDoc}
066     */
067    protected CheckOutScmResult executeCheckOutCommand( ScmProviderRepository repo, ScmFileSet files,
068                                                        ScmVersion version, boolean recursive )
069        throws ScmException
070    {
071        PerforceScmProviderRepository prepo = (PerforceScmProviderRepository) repo;
072        File workingDirectory = new File( files.getBasedir().getAbsolutePath() );
073
074        actualLocation = PerforceScmProvider.getRepoPath( getLogger(), prepo, files.getBasedir() );
075
076        String specname = PerforceScmProvider.getClientspecName( getLogger(), prepo, workingDirectory );
077        PerforceCheckOutConsumer consumer = new PerforceCheckOutConsumer( specname, actualLocation );
078        if ( getLogger().isInfoEnabled() )
079        {
080            getLogger().info( "Checkout working directory: " + workingDirectory );
081        }
082        Commandline cl = null;
083
084        // Create or update a clientspec so we can checkout the code to a particular location
085        try
086        {
087            // Ahhh, glorious Perforce.  Create and update of clientspecs is the exact
088            // same operation so we don't need to distinguish between the two modes.
089            cl = PerforceScmProvider.createP4Command( prepo, workingDirectory );
090            cl.createArg().setValue( "client" );
091            cl.createArg().setValue( "-i" );
092            if ( getLogger().isInfoEnabled() )
093            {
094                getLogger().info( "Executing: " + PerforceScmProvider.clean( cl.toString() ) );
095            }
096
097            String client =
098                PerforceScmProvider.createClientspec( getLogger(), prepo, workingDirectory, actualLocation );
099
100            if ( getLogger().isDebugEnabled() )
101            {
102                getLogger().debug( "Updating clientspec:\n" + client );
103            }
104
105            CommandLineUtils.StringStreamConsumer err = new CommandLineUtils.StringStreamConsumer();
106            int exitCode =
107                CommandLineUtils.executeCommandLine( cl, new ByteArrayInputStream( client.getBytes() ), consumer, err );
108
109            if ( exitCode != 0 )
110            {
111                String cmdLine = CommandLineUtils.toString( cl.getCommandline() );
112
113                StringBuilder msg = new StringBuilder( "Exit code: " + exitCode + " - " + err.getOutput() );
114                msg.append( '\n' );
115                msg.append( "Command line was:" + cmdLine );
116
117                throw new CommandLineException( msg.toString() );
118            }
119        }
120        catch ( CommandLineException e )
121        {
122            if ( getLogger().isErrorEnabled() )
123            {
124                getLogger().error( "CommandLineException " + e.getMessage(), e );
125            }
126        }
127
128        boolean clientspecExists = consumer.isSuccess();
129
130        // Perform the actual checkout using that clientspec
131        try
132        {
133            if ( clientspecExists )
134            {
135                try
136                {
137                    getLastChangelist( prepo, workingDirectory, specname );
138                    cl = createCommandLine( prepo, workingDirectory, version, specname );
139                    if ( getLogger().isDebugEnabled() )
140                    {
141                        getLogger().debug( "Executing: " + PerforceScmProvider.clean( cl.toString() ) );
142                    }
143                    Process proc = cl.execute();
144                    BufferedReader br = new BufferedReader( new InputStreamReader( proc.getInputStream() ) );
145                    String line;
146                    while ( ( line = br.readLine() ) != null )
147                    {
148                        if ( getLogger().isDebugEnabled() )
149                        {
150                            getLogger().debug( "Consuming: " + line );
151                        }
152                        consumer.consumeLine( line );
153                    }
154                    CommandLineUtils.StringStreamConsumer err = new CommandLineUtils.StringStreamConsumer();
155                    int exitCode = CommandLineUtils.executeCommandLine( cl, consumer, err );
156
157                    if ( exitCode != 0 )
158                    {
159                        String cmdLine = CommandLineUtils.toString( cl.getCommandline() );
160
161                        StringBuilder msg = new StringBuilder( "Exit code: " + exitCode + " - " + err.getOutput() );
162                        msg.append( '\n' );
163                        msg.append( "Command line was:" + cmdLine );
164
165                        throw new CommandLineException( msg.toString() );
166                    }
167                    if ( getLogger().isDebugEnabled() )
168                    {
169                        getLogger().debug( "Perforce sync complete." );
170                    }
171                }
172                catch ( CommandLineException e )
173                {
174                    if ( getLogger().isErrorEnabled() )
175                    {
176                        getLogger().error( "CommandLineException " + e.getMessage(), e );
177                    }
178                }
179                catch ( IOException e )
180                {
181                    if ( getLogger().isErrorEnabled() )
182                    {
183                        getLogger().error( "IOException " + e.getMessage(), e );
184                    }
185                }
186            }
187
188            if ( consumer.isSuccess() )
189            {
190                return new CheckOutScmResult( cl.toString(), consumer.getCheckedout() );
191            }
192            else
193            {
194                return new CheckOutScmResult( cl.toString(), "Unable to sync.  Are you logged in?",
195                                              consumer.getOutput(), consumer.isSuccess() );
196            }
197        }
198        finally
199        {
200            // See SCM-113
201            // Support transient clientspecs as we don't want to create 1000s of permanent clientspecs
202            if ( clientspecExists && !prepo.isPersistCheckout() )
203            {
204                // Delete the clientspec
205                InputStreamReader isReader = null;
206                InputStreamReader isReaderErr = null;
207                try
208                {
209                    cl = PerforceScmProvider.createP4Command( prepo, workingDirectory );
210                    cl.createArg().setValue( "client" );
211                    cl.createArg().setValue( "-d" );
212                    cl.createArg().setValue( specname );
213                    if ( getLogger().isInfoEnabled() )
214                    {
215                        getLogger().info( "Executing: " + PerforceScmProvider.clean( cl.toString() ) );
216                    }
217                    Process proc = cl.execute();
218                    isReader = new InputStreamReader( proc.getInputStream() );
219                    BufferedReader br = new BufferedReader( isReader );
220                    String line;
221                    while ( ( line = br.readLine() ) != null )
222                    {
223                        if ( getLogger().isDebugEnabled() )
224                        {
225                            getLogger().debug( "Consuming: " + line );
226                        }
227                        consumer.consumeLine( line );
228                    }
229                    br.close();
230                    // Read errors from STDERR
231                    isReaderErr = new InputStreamReader( proc.getErrorStream() );
232                    BufferedReader brErr = new BufferedReader( isReaderErr );
233                    while ( ( line = brErr.readLine() ) != null )
234                    {
235                        if ( getLogger().isDebugEnabled() )
236                        {
237                            getLogger().debug( "Consuming stderr: " + line );
238                        }
239                        consumer.consumeLine( line );
240                    }
241                    brErr.close();
242                }
243                catch ( CommandLineException e )
244                {
245                    if ( getLogger().isErrorEnabled() )
246                    {
247                        getLogger().error( "CommandLineException " + e.getMessage(), e );
248                    }
249                }
250                catch ( IOException e )
251                {
252                    if ( getLogger().isErrorEnabled() )
253                    {
254                        getLogger().error( "IOException " + e.getMessage(), e );
255                    }
256                }
257                finally
258                {
259                    IOUtil.close( isReader );
260                    IOUtil.close( isReaderErr );
261                }
262            }
263            else if ( clientspecExists )
264            {
265                // SCM-165 Save clientspec in memory so we can reuse it with further commands in this VM.
266                System.setProperty( PerforceScmProvider.DEFAULT_CLIENTSPEC_PROPERTY, specname );
267            }
268        }
269    }
270
271    public static Commandline createCommandLine( PerforceScmProviderRepository repo, File workingDirectory,
272                                                 ScmVersion version, String specname )
273    {
274        Commandline command = PerforceScmProvider.createP4Command( repo, workingDirectory );
275
276        command.createArg().setValue( "-c" + specname );
277        command.createArg().setValue( "sync" );
278
279        // Use a simple heuristic to determine if we should use the Force flag
280        // on sync.  Forcing sync is a HUGE performance hit but is required in
281        // rare instances where source is somehow deleted.  If the target
282        // directory is completely empty, assume a force is required.  If
283        // not empty, we assume a previous checkout was already done and a normal
284        // sync will suffice.
285        // SCM-110
286        String[] files = workingDirectory.list();
287        if ( files == null || files.length == 0 )
288        {
289            // We need to force so checkout to an empty directory will work.
290            command.createArg().setValue( "-f" );
291        }
292
293        // Not sure what to do here. I'm unclear whether we should be
294        // sync'ing each file individually to the label or just sync the
295        // entire contents of the workingDir. I'm going to assume the
296        // latter until the exact semantics are clearer.
297        if ( version != null && StringUtils.isNotEmpty( version.getName() ) )
298        {
299            command.createArg().setValue( "@" + version.getName() );
300        }
301        return command;
302    }
303
304    private int getLastChangelist( PerforceScmProviderRepository repo, File workingDirectory, String specname )
305    {
306        int lastChangelist = 0;
307        try
308        {
309            Commandline command = PerforceScmProvider.createP4Command( repo, workingDirectory );
310
311            command.createArg().setValue( "-c" + specname );
312            command.createArg().setValue( "changes" );
313            command.createArg().setValue( "-m1" );
314            command.createArg().setValue( "-ssubmitted" );
315            command.createArg().setValue( "//" + specname + "/..." );
316            getLogger().debug( "Executing: " + PerforceScmProvider.clean( command.toString() ) );
317            Process proc = command.execute();
318            BufferedReader br = new BufferedReader( new InputStreamReader( proc.getInputStream() ) );
319            String line;
320
321            String lastChangelistStr = "";
322            while ( ( line = br.readLine() ) != null )
323            {
324                getLogger().debug( "Consuming: " + line );
325                Pattern changeRegexp = Pattern.compile( "Change (\\d+)" );
326                Matcher matcher = changeRegexp.matcher( line );
327                if ( matcher.find() )
328                {
329                    lastChangelistStr = matcher.group( 1 );
330                }
331            }
332            br.close();
333            // TODO: Read errors from STDERR?
334
335            try
336            {
337                lastChangelist = Integer.parseInt( lastChangelistStr );
338            }
339            catch ( NumberFormatException nfe )
340            {
341                getLogger().debug( "Could not parse changelist from line " + line );
342            }
343        }
344        catch ( IOException e )
345        {
346            getLogger().error( e );
347        }
348        catch ( CommandLineException e )
349        {
350            getLogger().error( e );
351        }
352
353        return lastChangelist;
354    }
355}