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