001/*
002 * Copyright 2007 The Kuali Foundation
003 * 
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * 
008 * http://www.opensource.org/licenses/ecl2.php
009 * 
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kualigan.maven.plugins.api;
017
018import org.apache.commons.cli.CommandLine;
019import org.apache.commons.cli.OptionBuilder;
020import org.apache.commons.cli.Options;
021import org.apache.commons.cli.PosixParser;
022
023import org.apache.maven.archetype.Archetype;
024import org.apache.maven.artifact.repository.ArtifactRepository;
025import org.apache.maven.artifact.repository.ArtifactRepositoryFactory;
026import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy;
027import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout;
028
029import org.apache.maven.shared.invoker.DefaultInvocationRequest;
030import org.apache.maven.shared.invoker.DefaultInvoker;
031import org.apache.maven.shared.invoker.InvocationOutputHandler;
032import org.apache.maven.shared.invoker.InvocationRequest;
033import org.apache.maven.shared.invoker.InvocationResult;
034import org.apache.maven.shared.invoker.Invoker;
035import org.apache.maven.shared.invoker.InvokerLogger;
036import org.apache.maven.shared.invoker.MavenInvocationException;
037
038import org.apache.maven.plugin.AbstractMojo;
039import org.apache.maven.plugin.MojoExecutionException;
040import org.apache.maven.project.MavenProject;
041
042import org.codehaus.plexus.archiver.Archiver;
043import org.codehaus.plexus.archiver.ArchiverException;
044import org.codehaus.plexus.archiver.UnArchiver;
045import org.codehaus.plexus.archiver.manager.ArchiverManager;
046import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
047import org.codehaus.plexus.archiver.util.DefaultFileSet;
048import org.codehaus.plexus.components.io.fileselectors.IncludeExcludeFileSelector;
049
050import org.codehaus.plexus.component.annotations.Component;
051import org.codehaus.plexus.component.annotations.Requirement;
052import org.codehaus.plexus.components.interactivity.Prompter;
053import org.codehaus.plexus.util.FileUtils;
054import org.codehaus.plexus.util.IOUtil;
055import org.codehaus.plexus.util.StringUtils;
056import org.codehaus.plexus.util.cli.CommandLineUtils;
057
058import java.io.DataInputStream;
059import java.io.File;
060import java.io.FileOutputStream;
061import java.io.FileWriter;
062import java.io.InputStream;
063import java.util.ArrayList;
064import java.util.HashMap;
065import java.util.List;
066import java.util.Map;
067import java.util.Properties;
068import java.util.StringTokenizer;
069
070/**
071 * Creates a prototype from the given KFS project resource. A KFS project resource can be either
072 * of the following:
073 * <ul>
074 *   <li>KFS war file</li>
075 *   <li>KFS project directory with source</li>
076 *   <li>KFS svn repo</li>
077 * </ul>
078 * 
079 * @author Leo Przybylski
080 */
081@Component(role = org.kualigan.maven.plugins.api.PrototypeHelper.class, hint="default")
082public class DefaultPrototypeHelper implements PrototypeHelper {
083    public static final String ROLE_HINT = "default";
084    
085    private static final Options OPTIONS = new Options();
086
087    private static final char SET_SYSTEM_PROPERTY = 'D';
088
089    private static final char OFFLINE = 'o';
090
091    private static final char REACTOR = 'r';
092
093    private static final char QUIET = 'q';
094
095    private static final char DEBUG = 'X';
096
097    private static final char ERRORS = 'e';
098
099    private static final char NON_RECURSIVE = 'N';
100
101    private static final char UPDATE_SNAPSHOTS = 'U';
102
103    private static final char ACTIVATE_PROFILES = 'P';
104
105    private static final String FORCE_PLUGIN_UPDATES = "cpu";
106
107    private static final String FORCE_PLUGIN_UPDATES2 = "up";
108
109    private static final String SUPPRESS_PLUGIN_UPDATES = "npu";
110
111    private static final String SUPPRESS_PLUGIN_REGISTRY = "npr";
112
113    private static final char CHECKSUM_FAILURE_POLICY = 'C';
114
115    private static final char CHECKSUM_WARNING_POLICY = 'c';
116
117    private static final char ALTERNATE_USER_SETTINGS = 's';
118
119    private static final String FAIL_FAST = "ff";
120
121    private static final String FAIL_AT_END = "fae";
122
123    private static final String FAIL_NEVER = "fn";
124    
125    private static final String ALTERNATE_POM_FILE = "f";
126    
127    /**
128     */
129    @Requirement
130    protected Archetype archetype;
131
132    /**
133     */
134    @Requirement
135    protected Prompter prompter;
136
137    /**
138     */
139    @Requirement
140    protected ArtifactRepositoryFactory artifactRepositoryFactory;
141
142    /**
143     */
144    @Requirement(role=org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout.class, hint="default")
145    protected ArtifactRepositoryLayout defaultArtifactRepositoryLayout;
146
147    @Requirement
148    protected ArchiverManager archiverManager;
149    
150    private AbstractMojo caller;
151
152    protected void logUnpack(final File file, final File location, final String includes, final String excludes ) {
153        if (!getCaller().getLog().isInfoEnabled()) {
154            return;
155        }
156
157        StringBuffer msg = new StringBuffer();
158        msg.append( "Unpacking " );
159        msg.append( file );
160        msg.append( " to " );
161        msg.append( location );
162
163        if ( includes != null && excludes != null ) {
164            msg.append( " with includes \"" );
165            msg.append( includes );
166            msg.append( "\" and excludes \"" );
167            msg.append( excludes );
168            msg.append( "\"" );
169        }
170        else if (includes != null) {
171            msg.append( " with includes \"" );
172            msg.append( includes );
173            msg.append( "\"" );
174        }
175        else if (excludes != null) {
176            msg.append( " with excludes \"" );
177            msg.append( excludes );
178            msg.append( "\"" );
179        }
180
181        getCaller().getLog().info(msg.toString());
182    }
183    
184    /**
185     * Handles repacking of the war as a jar file with classes, etc... Basically makes a jar of everything
186     * in the war file's WEB-INF/classes path.
187     * 
188     * @param file is the war file to repackage
189     * @return {@link File} instance of the repacked jar
190     */
191    public File repack(final File file, final String artifactId) throws MojoExecutionException {
192        final File workingDirectory = new File(System.getProperty("java.io.tmpdir") + File.separator
193                + artifactId + "-repack");
194        final File warDirectory     = new File(workingDirectory, "war");
195        final File repackDirectory  = new File(workingDirectory, artifactId);
196        final File classesDirectory = new File(warDirectory, "WEB-INF/classes");
197        final File retval           = new File(workingDirectory, artifactId + ".jar");
198        
199        try {
200            workingDirectory.mkdirs();
201            workingDirectory.mkdir();
202            workingDirectory.deleteOnExit();
203            warDirectory.mkdir();
204        }
205        catch (Exception e) {
206            throw new MojoExecutionException("Unable to create working directory for repackaging", e);
207        }
208        
209        unpack(file, warDirectory, "**/classes/**", null);
210        
211        try {
212            FileUtils.copyDirectoryStructure(classesDirectory, repackDirectory);
213        }
214        catch (Exception e) {
215            throw new MojoExecutionException("Unable to copy files into the repack directory");
216        }
217
218        try {
219            pack(retval, repackDirectory, "**/**", null);
220        }
221        catch (Exception e) {
222            throw new MojoExecutionException("Was unable to create the jar", e);
223        }
224        
225        return retval;
226    }
227    
228    /**
229     * Unpacks the archive file.
230     *
231     * @param file     File to be unpacked.
232     * @param location Location where to put the unpacked files.
233     * @param includes Comma separated list of file patterns to include i.e. <code>**&#47;.xml,
234     *                 **&#47;*.properties</code>
235     * @param excludes Comma separated list of file patterns to exclude i.e. <code>**&#47;*.xml,
236     *                 **&#47;*.properties</code>
237     */
238    protected void unpack(final File file, final File location, final String includes, final String excludes) throws MojoExecutionException {
239        try {
240            logUnpack(file, location, includes, excludes);
241
242            location.mkdirs();
243
244            final UnArchiver unArchiver;
245            unArchiver = archiverManager.getUnArchiver(file);
246            unArchiver.setSourceFile(file);
247            unArchiver.setDestDirectory(location);
248
249            if (StringUtils.isNotEmpty(excludes) || StringUtils.isNotEmpty(includes)) {
250                final IncludeExcludeFileSelector[] selectors =
251                    new IncludeExcludeFileSelector[]{ new IncludeExcludeFileSelector() };
252
253                if (StringUtils.isNotEmpty( excludes )) {
254                    selectors[0].setExcludes(excludes.split( "," ));
255                }
256
257                if (StringUtils.isNotEmpty( includes )) {
258                    selectors[0].setIncludes(includes.split( "," ));
259                }
260
261                unArchiver.setFileSelectors(selectors);
262            }
263
264            unArchiver.extract();
265        }
266        catch ( NoSuchArchiverException e ) {
267            throw new MojoExecutionException("Unknown archiver type", e);
268        }
269        catch (ArchiverException e) {
270            e.printStackTrace();
271            throw new MojoExecutionException(
272                "Error unpacking file: " + file + " to: " + location + "\r\n" + e.toString(), e );
273        }
274    }
275
276    /**
277     * Packs a jar
278     *
279     * @param file     Destination file.
280     * @param location Directory source.
281     * @param includes Comma separated list of file patterns to include i.e. <code>**&#47;.xml,
282     *                 **&#47;*.properties</code>
283     * @param excludes Comma separated list of file patterns to exclude i.e. <code>**&#47;*.xml,
284     *                 **&#47;*.properties</code>
285     */
286    protected void pack(final File file, final File location, final String includes, final String excludes) throws MojoExecutionException {
287        try {
288
289            final Archiver archiver;
290            archiver = archiverManager.getArchiver(file);
291            archiver.addFileSet(new DefaultFileSet() {{
292                    setDirectory(location);
293                    if (includes != null) {
294                        setIncludes(includes.split(","));
295                    }
296                    if (excludes != null) {
297                        setExcludes(excludes.split(","));
298                    }
299                }});
300            archiver.setDestFile(file);
301
302            archiver.createArchive();
303        }
304        catch ( NoSuchArchiverException e ) {
305            throw new MojoExecutionException("Unknown archiver type", e);
306        }
307        catch (Exception e) {
308            e.printStackTrace();
309            throw new MojoExecutionException(
310                "Error packing directory: " + location + " to: " + file + "\r\n" + e.toString(), e );
311        }
312    }
313
314    /**
315     * 
316     */
317    private void setupRequest(final InvocationRequest req,
318                              final String additionalArguments) throws MojoExecutionException {
319        try {
320            final String[] args = CommandLineUtils.translateCommandline(additionalArguments);
321            CommandLine cli = new PosixParser().parse(OPTIONS, args);
322
323            if (cli.hasOption( SET_SYSTEM_PROPERTY))
324            {
325                String[] properties = cli.getOptionValues( SET_SYSTEM_PROPERTY );
326                Properties props = new Properties();
327                for ( int i = 0; i < properties.length; i++ )
328                {
329                    String property = properties[i];
330                    String name, value;
331                    int sep = property.indexOf( "=" );
332                    if ( sep <= 0 )
333                    {
334                        name = property.trim();
335                        value = "true";
336                    }
337                    else
338                    {
339                        name = property.substring( 0, sep ).trim();
340                        value = property.substring( sep + 1 ).trim();
341                    }
342                    props.setProperty( name, value );
343                }
344
345                req.setProperties( props );
346            }
347
348            if ( cli.hasOption( OFFLINE ) )
349            {
350                req.setOffline( true );
351            }
352
353            if ( cli.hasOption( QUIET ) )
354            {
355                // TODO: setQuiet() currently not supported by InvocationRequest
356                req.setDebug( false );
357            }
358            else if ( cli.hasOption( DEBUG ) )
359            {
360                req.setDebug( true );
361            }
362            else if ( cli.hasOption( ERRORS ) )
363            {
364                req.setShowErrors( true );
365            }
366
367            if ( cli.hasOption( REACTOR ) )
368            {
369                req.setRecursive( true );
370            }
371            else if ( cli.hasOption( NON_RECURSIVE ) )
372            {
373                req.setRecursive( false );
374            }
375
376            if ( cli.hasOption( UPDATE_SNAPSHOTS ) )
377            {
378                req.setUpdateSnapshots( true );
379            }
380
381            if ( cli.hasOption( ACTIVATE_PROFILES ) )
382            {
383                String[] profiles = cli.getOptionValues( ACTIVATE_PROFILES );
384                List<String> activatedProfiles = new ArrayList<String>();
385                List<String> deactivatedProfiles = new ArrayList<String>();
386
387                if ( profiles != null )
388                {
389                    for ( int i = 0; i < profiles.length; ++i )
390                    {
391                        StringTokenizer profileTokens = new StringTokenizer( profiles[i], "," );
392
393                        while ( profileTokens.hasMoreTokens() )
394                        {
395                            String profileAction = profileTokens.nextToken().trim();
396
397                            if ( profileAction.startsWith( "-" ) || profileAction.startsWith( "!" ) )
398                            {
399                                deactivatedProfiles.add( profileAction.substring( 1 ) );
400                            }
401                            else if ( profileAction.startsWith( "+" ) )
402                            {
403                                activatedProfiles.add( profileAction.substring( 1 ) );
404                            }
405                            else
406                            {
407                                activatedProfiles.add( profileAction );
408                            }
409                        }
410                    }
411                }
412
413                if (!deactivatedProfiles.isEmpty()) {
414                    getCaller().getLog().warn("Explicit profile deactivation is not yet supported. "
415                                                + "The following profiles will NOT be deactivated: " + StringUtils.join(
416                                                deactivatedProfiles.iterator(), ", "));
417                }
418
419                if (!activatedProfiles.isEmpty()) {
420                    req.setProfiles(activatedProfiles);
421                }
422            }
423
424            if (cli.hasOption(FORCE_PLUGIN_UPDATES) || cli.hasOption( FORCE_PLUGIN_UPDATES2)) {
425                getCaller().getLog().warn("Forcing plugin updates is not supported currently.");
426            }
427            else if (cli.hasOption( SUPPRESS_PLUGIN_UPDATES)) {
428                req.setNonPluginUpdates( true );
429            }
430
431            if (cli.hasOption( SUPPRESS_PLUGIN_REGISTRY)) {
432                getCaller().getLog().warn("Explicit suppression of the plugin registry is not supported currently." );
433            }
434
435            if (cli.hasOption(CHECKSUM_FAILURE_POLICY)) {
436                req.setGlobalChecksumPolicy( InvocationRequest.CHECKSUM_POLICY_FAIL );
437            }
438            else if ( cli.hasOption( CHECKSUM_WARNING_POLICY ) )
439            {
440                req.setGlobalChecksumPolicy( InvocationRequest.CHECKSUM_POLICY_WARN );
441            }
442
443            if ( cli.hasOption( ALTERNATE_USER_SETTINGS ) )
444            {
445                req.setUserSettingsFile( new File( cli.getOptionValue( ALTERNATE_USER_SETTINGS ) ) );
446            }
447
448            if ( cli.hasOption( FAIL_AT_END ) )
449            {
450                req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_AT_END );
451            }
452            else if ( cli.hasOption( FAIL_FAST ) )
453            {
454                req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_FAST );
455            }
456            if ( cli.hasOption( FAIL_NEVER ) )
457            {
458                req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_NEVER );
459            }
460            if ( cli.hasOption( ALTERNATE_POM_FILE ) )
461            {
462                if (req.getPomFileName() != null) {
463                    getCaller().getLog().info("pomFileName is already set, ignoring the -f argument" );
464                }
465                else {
466                    req.setPomFileName( cli.getOptionValue( ALTERNATE_POM_FILE ) );
467                }
468            }
469        }
470        catch ( Exception e ) {
471            throw new MojoExecutionException("Failed to re-parse additional arguments for Maven invocation.", e );
472        }
473    }
474    
475    /**
476     * Executes the {@code install-file} goal with the new pom against the artifact file.
477     * 
478     * @param artifact {@link File} instance to install
479     */
480    public void installArtifact(final File artifact, 
481                                final File sources,
482                                final File mavenHome,
483                                final String groupId,
484                                final String artifactId,
485                                final String version,
486                                final String repositoryId) throws MojoExecutionException {
487        final Invoker invoker = new DefaultInvoker().setMavenHome(mavenHome);
488        
489        final String additionalArguments = "";
490
491        final InvocationRequest req = new DefaultInvocationRequest()
492                .setInteractive(false)
493                .setProperties(new Properties() {{
494                        setProperty("groupId", groupId);
495                        setProperty("artifactId", artifactId);
496                        setProperty("version", version);
497                        setProperty("packaging", artifact.getName().endsWith("jar") ? "jar" : "war");
498                        setProperty("pomFile", getTempPomPath());
499                        if (repositoryId != null) {
500                            setProperty("repositoryId", repositoryId);
501                        }
502                        if (sources != null) {
503                            try {
504                                setProperty("sources", sources.getCanonicalPath());
505                            }
506                            catch (Exception e) {
507                                throw new MojoExecutionException("Cannot get path for the sources file ", e);
508                            }                            
509                        }
510                        try {
511                            setProperty("file", artifact.getCanonicalPath());
512                        }
513                        catch (Exception e) {
514                            throw new MojoExecutionException("Cannot get path for the war file ", e);
515                        }
516                        setProperty("updateReleaseInfo", "true");
517                    }});
518
519        try {
520            setupRequest(req, additionalArguments);
521
522            if (repositoryId == null) {
523                req.setGoals(new ArrayList<String>() {{ add("install:install-file"); }});
524            }
525            else {
526                req.setGoals(new ArrayList<String>() {{ add("deploy:deploy-file"); }});
527            }
528
529            try {
530                final InvocationResult invocationResult = invoker.execute(req);
531
532                if ( invocationResult.getExecutionException() != null ) {
533                    throw new MojoExecutionException("Error executing Maven.",
534                                                     invocationResult.getExecutionException());
535                }
536                    
537                if (invocationResult.getExitCode() != 0) {
538                    throw new MojoExecutionException(
539                        "Maven execution failed, exit code: \'" + invocationResult.getExitCode() + "\'");
540                }
541            }
542            catch (MavenInvocationException e) {
543                throw new MojoExecutionException( "Failed to invoke Maven build.", e );
544            }
545        }
546        finally {
547            /*
548            if ( settingsFile != null && settingsFile.exists() && !settingsFile.delete() )
549            {
550                settingsFile.deleteOnExit();
551            }
552            */
553        }
554    }
555
556    /**
557     * Temporary POM location
558     * 
559     * @return String value the path of the temporary POM
560     */
561    protected String getTempPomPath() {
562        return System.getProperty("java.io.tmpdir") + File.separator + "prototype-pom.xml";
563    }
564    
565    /**
566     * Puts a POM file in the system temp directory for prototype-pom.xml. prototype-pom.xml is extracted
567     * from the plugin.
568     */
569    public void extractTempPom() throws MojoExecutionException {
570        getCaller().getLog().info("Extracting the Temp POM");
571        
572        final InputStream pom_is = getClass().getClassLoader().getResourceAsStream("prototype-resources/pom.xml");
573        
574        byte[] fileBytes = null;
575        try {
576            final DataInputStream dis = new DataInputStream(pom_is);
577            fileBytes = new byte[dis.available()];
578            dis.readFully(fileBytes);
579            dis.close();
580        }
581        catch (Exception e) {
582            throw new MojoExecutionException("Wasn't able to read in the prototype pom", e);
583        }
584        finally {
585            try {
586                pom_is.close();
587            }
588            catch (Exception e) {
589                // Ignore exceptions
590            }
591        }
592        
593        try {
594            final FileOutputStream fos = new FileOutputStream(getTempPomPath());
595            try {
596                fos.write(fileBytes);
597            }
598            finally {
599                fos.close();
600            }
601        }
602        catch (Exception e) {
603            throw new MojoExecutionException("Could not write temporary pom file", e);
604        }
605    }
606    
607    public void setCaller(final AbstractMojo caller) {
608        this.caller = caller;
609    }
610    
611    public AbstractMojo getCaller() {
612        return this.caller;
613    }
614}