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        filterTempPom(groupId, artifactId, artifact.getName().endsWith("jar") ? "jar" : "war", version);
504
505        final Invoker invoker = new DefaultInvoker().setMavenHome(mavenHome);
506        
507        final String additionalArguments = "";
508
509        getCaller().getLog().debug("Setting up properties for installing the artifact");
510        final InvocationRequest req = new DefaultInvocationRequest()
511            .setInteractive(false)
512            .setProperties(new Properties() {{
513                                                        setProperty("pomFile", getTempPomPath());
514                                                        if (repositoryId != null) {
515                                                                setProperty("repositoryId", repositoryId);
516                                                        }
517                                                        if (sources != null) {
518                                                                try {
519                                                                        setProperty("sources", zipSourcesIfRequired(sources).getCanonicalPath());
520                                                                } catch (Exception e) {
521                                                                        throw new MojoExecutionException("Cannot get path for the sources file ", e);
522                                                                }
523                                                        }
524                                                        try {
525                                                                setProperty("file", artifact.getCanonicalPath());
526                                                        } catch (Exception e) {
527                                                                throw new MojoExecutionException("Cannot get path for the war file ", e);
528                                                        }
529                                                        setProperty("updateReleaseInfo", "true");
530                                                }});
531
532        getCaller().getLog().debug("Properties used for installArtifact are:");
533        try {
534            req.getProperties().list(System.out);
535        }
536        catch (Exception e) {
537        }
538
539        try {
540            setupRequest(req, additionalArguments);
541
542            if (repositoryId == null) {
543                req.setGoals(new ArrayList<String>() {{ add("install:install-file"); }});
544            }
545            else {
546                req.setGoals(new ArrayList<String>() {{ add("deploy:deploy-file"); }});
547            }
548
549            try {
550                final InvocationResult invocationResult = invoker.execute(req);
551
552                if ( invocationResult.getExecutionException() != null ) {
553                    throw new MojoExecutionException("Error executing Maven.",
554                                                     invocationResult.getExecutionException());
555                }
556                    
557                if (invocationResult.getExitCode() != 0) {
558                    throw new MojoExecutionException(
559                        "Maven execution failed, exit code: \'" + invocationResult.getExitCode() + "\'");
560                }
561            }
562            catch (MavenInvocationException e) {
563                throw new MojoExecutionException( "Failed to invoke Maven build.", e );
564            }
565        }
566        finally {
567            /*
568              if ( settingsFile != null && settingsFile.exists() && !settingsFile.delete() )
569              {
570              settingsFile.deleteOnExit();
571              }
572            */
573        }
574    }
575
576    /**
577     * Executes the {@code install-file} goal with the new pom against the artifact file.
578     * 
579     */
580    public void filterTempPom(final String groupId,
581                              final String artifactId,
582                              final String packaging,
583                              final String version) throws MojoExecutionException {
584        getCaller().getLog().info("Filtering the Temp POM");
585        
586        Writer writer = null;
587        Reader reader = null;
588        try {
589            Context context = new VelocityContext();
590            context.put("groupId", groupId);
591            context.put("artifactId", artifactId);
592            context.put("packaging", packaging);
593            context.put("version", version);
594            
595            writer = new FileWriter(System.getProperty("java.io.tmpdir") + File.separator + "pom.xml");
596            reader = new FileReader(new File(System.getProperty("java.io.tmpdir") + File.separator + "prototype-pom.xml"));
597            
598            Velocity.init();
599            Velocity.evaluate(context, writer, "pom-prototype", reader);
600        } 
601        catch (Exception e) {
602            throw new MojoExecutionException("Error trying to filter the pom ", e);
603        } 
604        finally {
605            IOUtils.closeQuietly(reader);
606            IOUtils.closeQuietly(writer);
607        }
608    }
609    
610    /**
611     * Temporary POM location
612     * 
613     * @return String value the path of the temporary POM
614     */
615    protected String getTempPomPath() {
616        return System.getProperty("java.io.tmpdir") + File.separator + "pom.xml";
617    }
618
619    /**
620     * Puts temporary pom in the system temp directory. prototype-pom.xml is extracted
621     * from the plugin.
622     */
623    public void extractTempPom() throws MojoExecutionException {
624        getCaller().getLog().info("Extracting the Temp Pom");
625        
626        final InputStream pom_is = getClass().getClassLoader().getResourceAsStream("prototype-resources/pom.xml");
627        
628        byte[] fileBytes = null;
629        try {
630            final DataInputStream dis = new DataInputStream(pom_is);
631            fileBytes = new byte[dis.available()];
632            dis.readFully(fileBytes);
633            dis.close();
634        }
635        catch (Exception e) {
636            throw new MojoExecutionException("Wasn't able to read in the prototype pom", e);
637        }
638        finally {
639            try {
640                pom_is.close();
641            }
642            catch (Exception e) {
643                // Ignore exceptions
644            }
645        }
646        
647        try {
648            final FileOutputStream fos = new FileOutputStream(System.getProperty("java.io.tmpdir") + File.separator + "pom.xml");
649            try {
650                fos.write(fileBytes);
651            }
652            finally {
653                fos.close();
654            }
655        }
656        catch (Exception e) {
657            throw new MojoExecutionException("Could not write temporary pom file", e);
658        }
659    }
660    
661    protected File zipSourcesIfRequired(File sources) {
662        if (sources.isFile()){
663            //Already a zip
664            return sources;
665        }
666        final File zipFile = new File(System.getProperty("java.io.tmpdir") + File.separator + "sources.zip");
667        zipFile.deleteOnExit();
668        ZipArchiver zipArchiver = new ZipArchiver();
669        zipArchiver.addDirectory(sources, new String[]{"**/*"}, new String[]{});
670        zipArchiver.setDestFile(zipFile);
671        try {
672            zipArchiver.createArchive();
673        } catch (IOException e) {
674            throw new RuntimeException("Unable to zip source directory", e);
675        }
676        return zipFile;
677    }
678
679    public void setCaller(final AbstractMojo caller) {
680        this.caller = caller;
681    }
682    
683    public AbstractMojo getCaller() {
684        return this.caller;
685    }
686
687}