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