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