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