View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with this
4    * work for additional information regarding copyright ownership. The ASF
5    * licenses this file to you under the Apache License, Version 2.0 (the
6    * "License"); you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
9    * or agreed to in writing, software distributed under the License is
10   * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11   * KIND, either express or implied. See the License for the specific language
12   * governing permissions and limitations under the License.
13   */
14  package org.apache.maven.plugin.eclipse.writers;
15  
16  import java.io.File;
17  import java.io.FileInputStream;
18  import java.io.FileOutputStream;
19  import java.io.IOException;
20  import java.util.Arrays;
21  import java.util.HashSet;
22  import java.util.Iterator;
23  import java.util.Set;
24  import java.util.jar.Attributes;
25  import java.util.jar.Manifest;
26  
27  import org.apache.maven.artifact.repository.ArtifactRepository;
28  import org.apache.maven.plugin.MojoExecutionException;
29  import org.apache.maven.plugin.eclipse.Constants;
30  import org.apache.maven.plugin.eclipse.EclipseSourceDir;
31  import org.apache.maven.plugin.eclipse.Messages;
32  import org.apache.maven.plugin.eclipse.writers.wtp.AbstractWtpResourceWriter;
33  import org.apache.maven.plugin.ide.IdeDependency;
34  import org.apache.maven.plugin.logging.Log;
35  import org.apache.maven.project.MavenProject;
36  
37  /**
38   * Create or adapt the manifest files for the RAD6 runtime dependencys. attention these will not be used for the real
39   * ear these are just to get the runtime enviorment using the maven dependencies. WARNING: The manifest resources added
40   * here will not have the benefit of the dependencies of the project, since that's not provided in the setup() apis, one
41   * of the locations from which this writer is used in the RadPlugin.
42   * 
43   * @author <a href="mailto:nir@cfc.at">Richard van Nieuwenhoven </a>
44   */
45  public class EclipseManifestWriter
46      extends AbstractEclipseWriter
47  {
48  
49      private static final String MANIFEST_MF_FILENAME = "MANIFEST.MF";
50  
51      private static final String META_INF_DIRECTORY = "META-INF";
52  
53      private static final String GENERATED_RESOURCE_DIRNAME =
54          "target" + File.separatorChar + "generated-resources" + File.separatorChar + "eclipse";
55  
56      private static final String WEBAPP_RESOURCE_DIR =
57          "src" + File.separatorChar + "main" + File.separatorChar + "webapp";
58  
59      /**
60       * Search the project for the existing META-INF directory where the manifest should be located.
61       * 
62       * @return the apsolute path to the META-INF directory
63       */
64      public String getMetaInfBaseDirectory( MavenProject project )
65      {
66          String metaInfBaseDirectory = null;
67  
68          if ( this.config.getProject().getPackaging().equals( Constants.PROJECT_PACKAGING_WAR ) )
69          {
70              metaInfBaseDirectory =
71                  this.config.getProject().getBasedir().getAbsolutePath() + File.separatorChar +
72                      EclipseManifestWriter.WEBAPP_RESOURCE_DIR;
73  
74              this.log.debug( "Attempting to use: " + metaInfBaseDirectory + " for location of META-INF in war project." );
75  
76              File metaInfDirectoryFile =
77                  new File( metaInfBaseDirectory + File.separatorChar + EclipseManifestWriter.META_INF_DIRECTORY );
78  
79              if ( metaInfDirectoryFile.exists() && !metaInfDirectoryFile.isDirectory() )
80              {
81                  metaInfBaseDirectory = null;
82              }
83          }
84  
85          for ( int index = this.config.getSourceDirs().length - 1; metaInfBaseDirectory == null && index >= 0; index-- )
86          {
87  
88              File manifestFile =
89                  new File( this.config.getEclipseProjectDirectory(), this.config.getSourceDirs()[index].getPath() +
90                      File.separatorChar + EclipseManifestWriter.META_INF_DIRECTORY + File.separatorChar +
91                      EclipseManifestWriter.MANIFEST_MF_FILENAME );
92  
93              this.log.debug( "Checking for existence of META-INF/MANIFEST.MF file: " + manifestFile );
94  
95              if ( manifestFile.exists() )
96              {
97                  metaInfBaseDirectory = manifestFile.getParentFile().getParent();
98              }
99          }
100 
101         return metaInfBaseDirectory;
102     }
103 
104     /**
105      * Write the manifest files use an existing one it it exists (it will be overwritten!! in a war use webapp/META-INF
106      * else use the generated rad6 sourcefolder
107      * 
108      * @see AbstractWtpResourceWriter#write(EclipseSourceDir[], ArtifactRepository, File)
109      * @param sourceDirs all eclipse source directorys
110      * @param localRepository the local reposetory
111      * @param buildOutputDirectory build output directory (target)
112      * @throws MojoExecutionException when writing the config files was not possible
113      */
114     public void write()
115         throws MojoExecutionException
116     {
117         String metaInfBaseDirectory = getMetaInfBaseDirectory( this.config.getProject() );
118 
119         if ( metaInfBaseDirectory == null )
120         {
121             // TODO: if this really is an error, shouldn't we stop the build??
122             throw new MojoExecutionException(
123                                               Messages.getString(
124                                                                   "EclipseCleanMojo.nofilefound",
125                                                                   new Object[] { EclipseManifestWriter.META_INF_DIRECTORY } ) );
126         }
127         // if
128         // (this.config.getEclipseProjectName().equals(IdeUtils.getProjectName(IdeUtils.PROJECT_NAME_WITH_VERSION_TEMPLATE,
129         // this.config.getProject()))) {
130         // MavenArchiver mavenArchiver = new MavenArchiver();
131         // ManifestConfiguration configuration = new ManifestConfiguration() {
132         //
133         // public boolean isAddClasspath() {
134         // return true;
135         // }
136         // };
137         //
138         // File manifestFile = new File(metaInfBaseDirectory + File.separatorChar +
139         // EclipseManifestWriter.META_INF_DIRECTORY + File.separatorChar + EclipseManifestWriter.MANIFEST_MF_FILENAME);
140         // manifestFile.getParentFile().mkdirs();
141         //
142         // try {
143         // PrintWriter printwriter = new PrintWriter(manifestFile);
144         // mavenArchiver.getManifest(this.config.getProject(), configuration).write(printwriter);
145         // printwriter.close();
146         // } catch (Exception e) {
147         // this.log.error(Messages.getString("EclipsePlugin.cantwritetofile", new Object[]{
148         // metaInfBaseDirectory + File.separatorChar + EclipseManifestWriter.MANIFEST_MF_FILENAME
149         // }));
150         // }
151         // } else {
152         Manifest manifest = createNewManifest();
153 
154         File manifestFile =
155             new File( metaInfBaseDirectory + File.separatorChar + EclipseManifestWriter.META_INF_DIRECTORY +
156                 File.separatorChar + EclipseManifestWriter.MANIFEST_MF_FILENAME );
157 
158         System.out.println( "MANIFEST LOCATION: " + manifestFile );
159 
160         if ( shouldNewManifestFileBeWritten( manifest, manifestFile ) )
161         {
162             System.out.println( "Writing manifest..." );
163 
164             manifestFile.getParentFile().mkdirs();
165 
166             try
167             {
168                 FileOutputStream stream = new FileOutputStream( manifestFile );
169 
170                 manifest.write( stream );
171 
172                 stream.close();
173 
174             }
175             catch ( Exception e )
176             {
177                 this.log.error( Messages.getString( "EclipsePlugin.cantwritetofile",
178                                                     new Object[] { metaInfBaseDirectory + File.separatorChar +
179                                                         EclipseManifestWriter.MANIFEST_MF_FILENAME } ) );
180             }
181 
182             // }
183         }
184     }
185 
186     /**
187      * make room for a Manifest file. use a generated resource for JARS and for WARS use the manifest in the
188      * webapp/META-INF directory.
189      * 
190      * @throws MojoExecutionException
191      */
192     public static void addManifestResource( Log log, EclipseWriterConfig config )
193         throws MojoExecutionException
194     {
195 
196         EclipseManifestWriter manifestWriter = new EclipseManifestWriter();
197         manifestWriter.init( log, config );
198 
199         String packaging = config.getProject().getPackaging();
200 
201         String manifestDirectory = manifestWriter.getMetaInfBaseDirectory( config.getProject() );
202 
203         if ( !Constants.PROJECT_PACKAGING_EAR.equals( packaging ) &&
204             !Constants.PROJECT_PACKAGING_WAR.equals( packaging ) && manifestDirectory == null )
205         {
206 
207             String generatedResourceDir =
208                 config.getProject().getBasedir().getAbsolutePath() + File.separatorChar +
209                     EclipseManifestWriter.GENERATED_RESOURCE_DIRNAME;
210 
211             manifestDirectory = generatedResourceDir + File.separatorChar + "META-INF";
212 
213             try
214             {
215                 new File( manifestDirectory ).mkdirs();
216                 File manifestFile = new File( manifestDirectory + File.separatorChar + "MANIFEST.MF" );
217                 if ( manifestFile.exists() )
218                 {
219                     manifestFile.delete();
220                 }
221                 manifestFile.createNewFile();
222             }
223             catch ( IOException e )
224             {
225                 log.error( Messages.getString( "EclipsePlugin.cantwritetofile", new Object[] { manifestDirectory +
226                     File.separatorChar + "META-INF" + File.separatorChar + "MANIFEST.MF" } ) );
227             }
228 
229             log.debug( "Adding " + EclipseManifestWriter.GENERATED_RESOURCE_DIRNAME + " to eclipse sources " );
230 
231             EclipseSourceDir[] sourceDirs = config.getSourceDirs();
232             EclipseSourceDir[] newSourceDirs = new EclipseSourceDir[sourceDirs.length + 1];
233             System.arraycopy( sourceDirs, 0, newSourceDirs, 0, sourceDirs.length );
234             newSourceDirs[sourceDirs.length] =
235                 new EclipseSourceDir( EclipseManifestWriter.GENERATED_RESOURCE_DIRNAME, null, true, false, null, null,
236                                       false );
237             config.setSourceDirs( newSourceDirs );
238         }
239 
240         if ( Constants.PROJECT_PACKAGING_WAR.equals( packaging ) )
241         {
242             new File( config.getProject().getBasedir().getAbsolutePath() + File.separatorChar + "src" +
243                 File.separatorChar + "main" + File.separatorChar + "webapp" + File.separatorChar + "META-INF" ).mkdirs();
244         }
245 
246         // special case must be done first because it can add stuff to the
247         // classpath that will be
248         // written by the superclass
249         manifestWriter.write();
250     }
251 
252     /**
253      * Add one dependency to the black separated classpath stringbuffer. When the project is available in the reactor
254      * (current build) then the project is used else the jar representing the artifact. System dependencies will only be
255      * included if they are in this project.
256      * 
257      * @param classpath existing classpath to append
258      * @param dependency dependency to append as jar or as project
259      */
260     private void addDependencyToClassPath( StringBuffer classpath, IdeDependency dependency )
261     {
262         if ( !dependency.isTestDependency() && !dependency.isProvided() &&
263             !dependency.isSystemScopedOutsideProject( this.config.getProject() ) )
264         {
265 
266             // blank is the separator in manifest classpath's
267             if ( classpath.length() != 0 )
268             {
269                 classpath.append( ' ' );
270             }
271             // if the dependency is a workspace project add the project and not
272             // the jar
273             if ( !dependency.isReferencedProject() )
274             {
275                 classpath.append( dependency.getFile().getName() );
276             }
277             else
278             {
279                 classpath.append( dependency.getEclipseProjectName() + ".jar" );
280             }
281         }
282     }
283 
284     /**
285      * Check if the two manifests are equal. Manifest.equal can not be used because of the special case the Classpath
286      * entr, witch must be comaired sorted so that a different oder in the classpath does not result in "not equal".
287      * This not not realy correct but in this case it is more important to reduce the number of version-controll files.
288      * 
289      * @param manifest the new manifest
290      * @param existingManifest to compaire the new one with
291      * @return are the manifests equal
292      */
293     private boolean areManifestsEqual( Manifest manifest, Manifest existingManifest )
294     {
295         if ( existingManifest == null )
296         {
297             return false;
298         }
299 
300         Set keys = new HashSet();
301         Attributes existingMap = existingManifest.getMainAttributes();
302         Attributes newMap = manifest.getMainAttributes();
303         keys.addAll( existingMap.keySet() );
304         keys.addAll( newMap.keySet() );
305         Iterator iterator = keys.iterator();
306         while ( iterator.hasNext() )
307         {
308             Attributes.Name key = (Attributes.Name) iterator.next();
309             String newValue = (String) newMap.get( key );
310             String existingValue = (String) existingMap.get( key );
311             // special case classpath... they are qual when there entries
312             // are equal
313             if ( Attributes.Name.CLASS_PATH.equals( key ) )
314             {
315                 newValue = orderClasspath( newValue );
316                 existingValue = orderClasspath( existingValue );
317             }
318             if ( ( newValue == null || !newValue.equals( existingValue ) ) &&
319                 ( existingValue == null || !existingValue.equals( newValue ) ) )
320             {
321                 return false;
322             }
323         }
324         return true;
325     }
326 
327     /**
328      * Convert all dependencies in a blank seperated list of jars and projects representing the classpath.
329      * 
330      * @return the blank separeted classpath string
331      */
332     private String constructManifestClasspath()
333     {
334         StringBuffer stringBuffer = new StringBuffer();
335         IdeDependency[] deps = this.config.getDepsOrdered();
336 
337         for ( int index = 0; index < deps.length; index++ )
338         {
339             addDependencyToClassPath( stringBuffer, deps[index] );
340         }
341 
342         return stringBuffer.toString();
343     }
344 
345     /**
346      * Create a manifest contaigning the required classpath.
347      * 
348      * @return the newly created manifest
349      */
350     private Manifest createNewManifest()
351     {
352         Manifest manifest = new Manifest();
353         manifest.getMainAttributes().put( Attributes.Name.MANIFEST_VERSION, "1.0" );
354         manifest.getMainAttributes().put( Attributes.Name.CLASS_PATH, constructManifestClasspath() );
355         return manifest;
356     }
357 
358     /**
359      * Aphabeticaly sort the classpath. Do this by splitting it up, sort the entries and gleue them together again.
360      * 
361      * @param newValue classpath to sort
362      * @return the sorted classpath
363      */
364     private String orderClasspath( String newValue )
365     {
366         if ( newValue == null )
367         {
368             return null;
369         }
370         String[] entries = newValue.split( " " );
371         Arrays.sort( entries );
372         StringBuffer buffer = new StringBuffer( newValue.length() );
373         for ( int index = 0; index < entries.length; index++ )
374         {
375             buffer.append( entries[index] );
376             buffer.append( ' ' );
377         }
378         return buffer.toString();
379     }
380 
381     /**
382      * Read and parse the existing manifest file.
383      * 
384      * @param manifestFile file
385      * @return the read manifest
386      * @throws IOException if the file could not be read
387      */
388     private Manifest readExistingManifest( File manifestFile )
389         throws IOException
390     {
391         if ( !manifestFile.exists() )
392         {
393             return null;
394         }
395 
396         Manifest existingManifest = new Manifest();
397         FileInputStream inputStream = new FileInputStream( manifestFile );
398         existingManifest.read( inputStream );
399         inputStream.close();
400         return existingManifest;
401     }
402 
403     /**
404      * Verify is the manifest sould be overwritten this sould take in account that the manifest should only be written
405      * if the contents of the classpath was changed not the order. The classpath sorting oder should be ignored.
406      * 
407      * @param manifest the newly created classpath
408      * @param manifestFile the file where the manifest
409      * @return if the new manifest file must be written
410      * @throws MojoExecutionException
411      */
412     private boolean shouldNewManifestFileBeWritten( Manifest manifest, File manifestFile )
413         throws MojoExecutionException
414     {
415         try
416         {
417             Manifest existingManifest = readExistingManifest( manifestFile );
418             if ( areManifestsEqual( manifest, existingManifest ) )
419             {
420                 this.log.info( Messages.getString( "EclipseCleanMojo.unchanged", manifestFile.getAbsolutePath() ) );
421                 return false;
422             }
423         }
424         catch ( Exception e )
425         {
426             throw new MojoExecutionException( Messages.getString( "EclipseCleanMojo.nofilefound",
427                                                                   manifestFile.getAbsolutePath() ), e );
428         }
429         return true;
430     }
431 }