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         String metaInfBaseDirectory = getMetaInfBaseDirectory( this.config.getProject() );
149 
150         if ( metaInfBaseDirectory == null )
151         {
152             // TODO: if this really is an error, shouldn't we stop the build??
153             throw new MojoExecutionException(
154                                               Messages.getString(
155                                                                   "EclipseCleanMojo.nofilefound",
156                                                                   new Object[] { EclipseManifestWriter.META_INF_DIRECTORY } ) );
157         }
158         manifestFile =
159             new File( metaInfBaseDirectory + File.separatorChar + EclipseManifestWriter.META_INF_DIRECTORY
160                 + File.separatorChar + EclipseManifestWriter.MANIFEST_MF_FILENAME );
161         Manifest manifest = createNewManifest();
162 
163         log.info( "MANIFEST LOCATION: " + manifestFile );
164 
165         if ( shouldNewManifestFileBeWritten( manifest, manifestFile ) )
166         {
167             log.info( "Writing manifest..." );
168 
169             manifestFile.getParentFile().mkdirs();
170 
171             try
172             {
173                 FileOutputStream stream = new FileOutputStream( manifestFile );
174 
175                 manifest.write( stream );
176 
177                 stream.close();
178 
179             }
180             catch ( Exception e )
181             {
182                 this.log.error( Messages.getString( "EclipsePlugin.cantwritetofile",
183                                                     new Object[] { manifestFile.getAbsolutePath() } ) );
184             }
185         }
186     }
187 
188     /**
189      * make room for a Manifest file. use a generated resource for JARS and for WARS use the manifest in the
190      * webapp/META-INF directory.
191      * 
192      * @throws MojoExecutionException
193      */
194     public static void addManifestResource( Log log, EclipseWriterConfig config )
195         throws MojoExecutionException
196     {
197 
198         EclipseManifestWriter manifestWriter = new EclipseManifestWriter();
199         manifestWriter.init( log, config );
200 
201         String packaging = config.getProject().getPackaging();
202 
203         String manifestDirectory = manifestWriter.getMetaInfBaseDirectory( config.getProject() );
204 
205         if ( !Constants.PROJECT_PACKAGING_EAR.equals( packaging )
206             && !Constants.PROJECT_PACKAGING_WAR.equals( packaging ) && manifestDirectory == null )
207         {
208 
209             String generatedResourceDir =
210                 config.getProject().getBasedir().getAbsolutePath() + File.separatorChar
211                     + EclipseManifestWriter.GENERATED_RESOURCE_DIRNAME;
212 
213             manifestDirectory = generatedResourceDir + File.separatorChar + "META-INF";
214 
215             try
216             {
217                 new File( manifestDirectory ).mkdirs();
218                 File manifestFile = new File( manifestDirectory + File.separatorChar + "MANIFEST.MF" );
219                 if ( manifestFile.exists() )
220                 {
221                     manifestFile.delete();
222                 }
223                 manifestFile.createNewFile();
224             }
225             catch ( IOException e )
226             {
227                 log.error( Messages.getString( "EclipsePlugin.cantwritetofile", new Object[] { manifestDirectory
228                     + File.separatorChar + "META-INF" + File.separatorChar + "MANIFEST.MF" } ) );
229             }
230 
231             log.debug( "Adding " + EclipseManifestWriter.GENERATED_RESOURCE_DIRNAME + " to eclipse sources " );
232 
233             EclipseSourceDir[] sourceDirs = config.getSourceDirs();
234             EclipseSourceDir[] newSourceDirs = new EclipseSourceDir[sourceDirs.length + 1];
235             System.arraycopy( sourceDirs, 0, newSourceDirs, 0, sourceDirs.length );
236             newSourceDirs[sourceDirs.length] =
237                 new EclipseSourceDir( EclipseManifestWriter.GENERATED_RESOURCE_DIRNAME, null, true, false, null, null,
238                                       false );
239             config.setSourceDirs( newSourceDirs );
240         }
241 
242         if ( Constants.PROJECT_PACKAGING_WAR.equals( packaging ) )
243         {
244             new File( getWebContentBaseDirectory( config ) + File.separatorChar + "META-INF" ).mkdirs();
245         }
246 
247         // special case must be done first because it can add stuff to the
248         // classpath that will be
249         // written by the superclass
250         manifestWriter.write();
251     }
252 
253     /**
254      * Add one dependency to the black separated classpath stringbuffer. When the project is available in the reactor
255      * (current build) then the project is used else the jar representing the artifact. System dependencies will only be
256      * included if they are in this project.
257      * 
258      * @param classpath existing classpath to append
259      * @param dependency dependency to append as jar or as project
260      */
261     private void addDependencyToClassPath( StringBuffer classpath, IdeDependency dependency )
262     {
263         if ( !dependency.isTestDependency() && !dependency.isProvided()
264             && !dependency.isSystemScopedOutsideProject( this.config.getProject() ) )
265         {
266 
267             // blank is the separator in manifest classpath's
268             if ( classpath.length() != 0 )
269             {
270                 classpath.append( ' ' );
271             }
272             // if the dependency is a workspace project add the project and not
273             // the jar
274             if ( !dependency.isReferencedProject() )
275             {
276                 classpath.append( dependency.getFile().getName() );
277             }
278             else
279             {
280                 classpath.append( dependency.getEclipseProjectName() + ".jar" );
281             }
282         }
283     }
284 
285     /**
286      * Check if the two manifests are equal. Manifest.equal can not be used because of the special case the Classpath
287      * entr, witch must be comaired sorted so that a different oder in the classpath does not result in "not equal".
288      * This not not realy correct but in this case it is more important to reduce the number of version-controll files.
289      * 
290      * @param manifest the new manifest
291      * @param existingManifest to compaire the new one with
292      * @return are the manifests equal
293      */
294     private boolean areManifestsEqual( Manifest manifest, Manifest existingManifest )
295     {
296         if ( existingManifest == null )
297         {
298             return false;
299         }
300 
301         Set keys = new HashSet();
302         Attributes existingMap = existingManifest.getMainAttributes();
303         Attributes newMap = manifest.getMainAttributes();
304         keys.addAll( existingMap.keySet() );
305         keys.addAll( newMap.keySet() );
306         Iterator iterator = keys.iterator();
307         while ( iterator.hasNext() )
308         {
309             Attributes.Name key = (Attributes.Name) iterator.next();
310             String newValue = (String) newMap.get( key );
311             String existingValue = (String) existingMap.get( key );
312             // special case classpath... they are qual when there entries
313             // are equal
314             if ( Attributes.Name.CLASS_PATH.equals( key ) )
315             {
316                 newValue = orderClasspath( newValue );
317                 existingValue = orderClasspath( existingValue );
318             }
319             if ( ( newValue == null || !newValue.equals( existingValue ) )
320                 && ( existingValue == null || !existingValue.equals( newValue ) ) )
321             {
322                 return false;
323             }
324         }
325         return true;
326     }
327 
328     /**
329      * Convert all dependencies in a blank seperated list of jars and projects representing the classpath.
330      * 
331      * @return the blank separeted classpath string
332      */
333     private String constructManifestClasspath()
334     {
335         StringBuffer stringBuffer = new StringBuffer();
336         IdeDependency[] deps = this.config.getDepsOrdered();
337 
338         for ( int index = 0; index < deps.length; index++ )
339         {
340             addDependencyToClassPath( stringBuffer, deps[index] );
341         }
342 
343         return stringBuffer.toString();
344     }
345 
346     /**
347      * Create a manifest contaigning the required classpath.
348      * 
349      * @return the newly created manifest
350      */
351     private Manifest createNewManifest()
352     {
353         Manifest manifest = new Manifest();
354         manifest.getMainAttributes().put( Attributes.Name.MANIFEST_VERSION, "1.0" );
355         manifest.getMainAttributes().put( Attributes.Name.CLASS_PATH, constructManifestClasspath() );
356         return manifest;
357     }
358 
359     /**
360      * Aphabeticaly sort the classpath. Do this by splitting it up, sort the entries and gleue them together again.
361      * 
362      * @param newValue classpath to sort
363      * @return the sorted classpath
364      */
365     private String orderClasspath( String newValue )
366     {
367         if ( newValue == null )
368         {
369             return null;
370         }
371         String[] entries = newValue.split( " " );
372         Arrays.sort( entries );
373         StringBuffer buffer = new StringBuffer( newValue.length() );
374         for ( int index = 0; index < entries.length; index++ )
375         {
376             buffer.append( entries[index] );
377             buffer.append( ' ' );
378         }
379         return buffer.toString();
380     }
381 
382     /**
383      * Read and parse the existing manifest file.
384      * 
385      * @param manifestFile file
386      * @return the read manifest
387      * @throws IOException if the file could not be read
388      */
389     private Manifest readExistingManifest( File manifestFile )
390         throws IOException
391     {
392         if ( !manifestFile.exists() )
393         {
394             return null;
395         }
396 
397         Manifest existingManifest = new Manifest();
398         FileInputStream inputStream = new FileInputStream( manifestFile );
399         existingManifest.read( inputStream );
400         inputStream.close();
401         return existingManifest;
402     }
403 
404     /**
405      * Verify is the manifest sould be overwritten this sould take in account that the manifest should only be written
406      * if the contents of the classpath was changed not the order. The classpath sorting oder should be ignored.
407      * 
408      * @param manifest the newly created classpath
409      * @param manifestFile the file where the manifest
410      * @return if the new manifest file must be written
411      * @throws MojoExecutionException
412      */
413     private boolean shouldNewManifestFileBeWritten( Manifest manifest, File manifestFile )
414         throws MojoExecutionException
415     {
416         try
417         {
418             Manifest existingManifest = readExistingManifest( manifestFile );
419             if ( areManifestsEqual( manifest, existingManifest ) )
420             {
421                 this.log.info( Messages.getString( "EclipseCleanMojo.unchanged", manifestFile.getAbsolutePath() ) );
422                 return false;
423             }
424         }
425         catch ( Exception e )
426         {
427             throw new MojoExecutionException( Messages.getString( "EclipseCleanMojo.nofilefound",
428                                                                   manifestFile.getAbsolutePath() ), e );
429         }
430         return true;
431     }
432 }