View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.felix.obrplugin;
20  
21  
22  import java.io.BufferedReader;
23  import java.io.File;
24  import java.io.FileNotFoundException;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.io.InputStreamReader;
28  import java.net.MalformedURLException;
29  import java.net.URI;
30  import java.net.URL;
31  import java.text.SimpleDateFormat;
32  import java.util.ArrayList;
33  import java.util.Arrays;
34  import java.util.Date;
35  import java.util.List;
36  import java.util.Properties;
37  import java.util.regex.Matcher;
38  import java.util.regex.Pattern;
39  
40  import javax.xml.parsers.DocumentBuilder;
41  import javax.xml.parsers.DocumentBuilderFactory;
42  import javax.xml.parsers.ParserConfigurationException;
43  import javax.xml.transform.Result;
44  import javax.xml.transform.Transformer;
45  import javax.xml.transform.TransformerConfigurationException;
46  import javax.xml.transform.TransformerException;
47  import javax.xml.transform.TransformerFactory;
48  import javax.xml.transform.dom.DOMSource;
49  import javax.xml.transform.stream.StreamResult;
50  
51  import org.apache.maven.artifact.manager.WagonManager;
52  import org.apache.maven.artifact.repository.ArtifactRepository;
53  import org.apache.maven.plugin.AbstractMojo;
54  import org.apache.maven.plugin.MojoExecutionException;
55  import org.apache.maven.plugin.logging.Log;
56  import org.apache.maven.plugins.annotations.Component;
57  import org.apache.maven.plugins.annotations.LifecyclePhase;
58  import org.apache.maven.plugins.annotations.Mojo;
59  import org.apache.maven.plugins.annotations.Parameter;
60  import org.apache.maven.project.MavenProject;
61  import org.apache.maven.settings.Settings;
62  import org.w3c.dom.Document;
63  import org.w3c.dom.Element;
64  import org.w3c.dom.Node;
65  import org.w3c.dom.NodeList;
66  import org.xml.sax.SAXException;
67  
68  
69  /**
70   * Clean a remote repository file.
71   * It just looks for every resources and check that pointed file exists.
72   *
73   * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
74   */
75  @Mojo( name = "remote-clean", requiresProject = false, defaultPhase = LifecyclePhase.CLEAN )
76  public final class ObrRemoteClean extends AbstractMojo
77  {
78      /**
79       * When true, ignore remote locking.
80       */
81      @Parameter( property = "ignoreLock" )
82      private boolean ignoreLock;
83  
84      /**
85       * Optional public URL prefix for the remote repository.
86       */
87      @Parameter( property = "prefixUrl" )
88      private String prefixUrl;
89  
90      /**
91       * Remote OBR Repository.
92       */
93      @Parameter( property = "remoteOBR", defaultValue = "NONE" )
94      private String remoteOBR;
95  
96      /**
97       * Local OBR Repository.
98       */
99      @Parameter( property = "obrRepository" )
100     private String obrRepository;
101 
102     /**
103      * Project types which this plugin supports.
104      */
105     @Parameter
106     private List supportedProjectTypes = Arrays.asList( new String[]
107         { "jar", "bundle" } );
108 
109     @Parameter( defaultValue = "${project.distributionManagementArtifactRepository}", readonly = true )
110     private ArtifactRepository deploymentRepository;
111 
112     /**
113      * Alternative deployment repository. Format: id::layout::url
114      */
115     @Parameter( property = "altDeploymentRepository" )
116     private String altDeploymentRepository;
117 
118     /**
119      * OBR specific deployment repository. Format: id::layout::url
120      */
121     @Parameter( property = "obrDeploymentRepository" )
122     private String obrDeploymentRepository;
123 
124     @Parameter( defaultValue = "${settings.interactiveMode}", readonly = true )
125     private boolean interactive;
126 
127     /**
128      * The Maven project.
129      */
130     @Parameter( defaultValue = "${project}", readonly = true, required = true )
131     private MavenProject project;
132 
133     /**
134      * Local Maven settings.
135      */
136     @Parameter( defaultValue = "${settings}", readonly = true, required = true )
137     private Settings settings;
138 
139     /**
140      * The Wagon manager.
141      */
142     @Component
143     private WagonManager m_wagonManager;
144 
145 
146     public void execute() throws MojoExecutionException
147     {
148         String projectType = project.getPackaging();
149 
150         // ignore unsupported project types, useful when bundleplugin is configured in parent pom
151         if ( !supportedProjectTypes.contains( projectType ) )
152         {
153             getLog().warn(
154                 "Ignoring project type " + projectType + " - supportedProjectTypes = " + supportedProjectTypes );
155             return;
156         }
157         else if ( "NONE".equalsIgnoreCase( remoteOBR ) || "false".equalsIgnoreCase( remoteOBR ) )
158         {
159             getLog().info( "Remote OBR update disabled (enable with -DremoteOBR)" );
160             return;
161         }
162 
163         // if the user doesn't supply an explicit name for the remote OBR file, use the local name instead
164         if ( null == remoteOBR || remoteOBR.trim().length() == 0 || "true".equalsIgnoreCase( remoteOBR ) )
165         {
166             remoteOBR = obrRepository;
167         }
168 
169         URI tempURI = ObrUtils.findRepositoryXml( "", remoteOBR );
170         String repositoryName = new File( tempURI.getSchemeSpecificPart() ).getName();
171 
172         Log log = getLog();
173 
174         RemoteFileManager remoteFile = new RemoteFileManager( m_wagonManager, settings, log );
175         openRepositoryConnection( remoteFile );
176         if ( null == prefixUrl )
177         {
178             prefixUrl = remoteFile.toString();
179         }
180 
181         // ======== LOCK REMOTE OBR ========
182         log.info( "LOCK " + remoteFile + '/' + repositoryName );
183         remoteFile.lockFile( repositoryName, ignoreLock );
184         File downloadedRepositoryXml = null;
185 
186         try
187         {
188             // ======== DOWNLOAD REMOTE OBR ========
189             log.info( "Downloading " + repositoryName );
190             downloadedRepositoryXml = remoteFile.get( repositoryName, ".xml" );
191 
192             URI repositoryXml = downloadedRepositoryXml.toURI();
193 
194             Config userConfig = new Config();
195             userConfig.setRemoteFile( true );
196 
197             // Clean the downloaded file.
198             Document doc = parseFile( new File( repositoryXml ), initConstructor() );
199             Node finalDocument = cleanDocument( doc.getDocumentElement() );
200 
201             if ( finalDocument == null )
202             {
203                 getLog().info( "Nothing to clean in " + repositoryName );
204             }
205             else
206             {
207                 writeToFile( repositoryXml, finalDocument ); // Write the new file
208                 getLog().info( "Repository " + repositoryName + " cleaned" );
209                 // ======== UPLOAD MODIFIED OBR ========
210                 log.info( "Uploading " + repositoryName );
211                 remoteFile.put( downloadedRepositoryXml, repositoryName );
212             }
213         }
214         catch ( Exception e )
215         {
216             log.warn( "Exception while updating remote OBR: " + e.getLocalizedMessage(), e );
217         }
218         finally
219         {
220             // ======== UNLOCK REMOTE OBR ========
221             log.info( "UNLOCK " + remoteFile + '/' + repositoryName );
222             remoteFile.unlockFile( repositoryName );
223             remoteFile.disconnect();
224 
225             if ( null != downloadedRepositoryXml )
226             {
227                 downloadedRepositoryXml.delete();
228             }
229         }
230     }
231 
232     private static final Pattern ALT_REPO_SYNTAX_PATTERN = Pattern.compile( "(.+)::(.+)::(.+)" );
233 
234 
235     private void openRepositoryConnection( RemoteFileManager remoteFile ) throws MojoExecutionException
236     {
237         // use OBR specific deployment location?
238         if ( obrDeploymentRepository != null )
239         {
240             altDeploymentRepository = obrDeploymentRepository;
241         }
242 
243         if ( deploymentRepository == null && altDeploymentRepository == null )
244         {
245             String msg = "Deployment failed: repository element was not specified in the pom inside"
246                 + " distributionManagement element or in -DaltDeploymentRepository=id::layout::url parameter";
247 
248             throw new MojoExecutionException( msg );
249         }
250 
251         if ( altDeploymentRepository != null )
252         {
253             getLog().info( "Using alternate deployment repository " + altDeploymentRepository );
254 
255             Matcher matcher = ALT_REPO_SYNTAX_PATTERN.matcher( altDeploymentRepository );
256             if ( !matcher.matches() )
257             {
258                 throw new MojoExecutionException( "Invalid syntax for alternative repository \""
259                     + altDeploymentRepository + "\". Use \"id::layout::url\"." );
260             }
261 
262             remoteFile.connect( matcher.group( 1 ).trim(), matcher.group( 3 ).trim() );
263         }
264         else
265         {
266             remoteFile.connect( deploymentRepository.getId(), deploymentRepository.getUrl() );
267         }
268     }
269 
270 
271     /**
272      * Analyze the given XML tree (DOM of the repository file) and remove missing resources.
273      * This method ask the user before deleting the resources from the repository.
274      * @param elem : the input XML tree
275      * @return the cleaned XML tree
276      */
277     private Element cleanDocument( Element elem )
278     {
279         NodeList nodes = elem.getElementsByTagName( "resource" );
280         List toRemove = new ArrayList();
281 
282         // First, look for missing resources
283         for ( int i = 0; i < nodes.getLength(); i++ )
284         {
285             Element n = ( Element ) nodes.item( i );
286             String value = n.getAttribute( "uri" );
287 
288             URL url;
289             try
290             {
291                 url = new URL( new URL( prefixUrl + '/' ), value );
292             }
293             catch ( MalformedURLException e )
294             {
295                 getLog().error( "Malformed URL when creating the resource absolute URI : " + e.getMessage() );
296                 return null;
297             }
298 
299             try
300             {
301                 url.openConnection().getContent();
302             }
303             catch ( IOException e )
304             {
305                 getLog().info(
306                     "The bundle " + n.getAttribute( "presentationname" ) + " - " + n.getAttribute( "version" )
307                         + " will be removed : " + e.getMessage() );
308                 toRemove.add( n );
309             }
310         }
311 
312         Date d = new Date();
313         if ( toRemove.size() > 0 )
314         {
315             String answer = "y";
316             if ( interactive )
317             {
318                 System.out.println( "Do you want to remove these bundles from the repository file [y/N]:" );
319                 BufferedReader br = new BufferedReader( new InputStreamReader( System.in ) );
320 
321                 try
322                 {
323                     answer = br.readLine();
324                 }
325                 catch ( IOException ioe )
326                 {
327                     getLog().error( "IO error trying to read the user confirmation" );
328                     return null;
329                 }
330             }
331 
332             if ( answer != null && answer.trim().equalsIgnoreCase( "y" ) )
333             {
334                 // Then remove missing resources.
335                 for ( int i = 0; i < toRemove.size(); i++ )
336                 {
337                     elem.removeChild( ( Node ) toRemove.get( i ) );
338                 }
339 
340                 // If we have to remove resources, we need to update 'lastmodified' attribute
341                 SimpleDateFormat format = new SimpleDateFormat( "yyyyMMddHHmmss.SSS" );
342                 d.setTime( System.currentTimeMillis() );
343                 elem.setAttribute( "lastmodified", format.format( d ) );
344                 return elem;
345             }
346             else
347             {
348                 return null;
349             }
350         }
351 
352         return null;
353     }
354 
355 
356     /**
357      * Initialize the document builder from Xerces.
358      *
359      * @return DocumentBuilder ready to create new document
360      * @throws MojoExecutionException : occurs when the instantiation of the document builder fails
361      */
362     private DocumentBuilder initConstructor() throws MojoExecutionException
363     {
364         DocumentBuilder constructor = null;
365         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
366         try
367         {
368             constructor = factory.newDocumentBuilder();
369         }
370         catch ( ParserConfigurationException e )
371         {
372             getLog().error( "Unable to create a new xml document" );
373             throw new MojoExecutionException( "Cannot create the Document Builder : " + e.getMessage() );
374         }
375         return constructor;
376     }
377 
378 
379     /**
380      * Open an XML file.
381      *
382      * @param file : XML file
383      * @param constructor DocumentBuilder get from xerces
384      * @return Document which describes this file
385      * @throws MojoExecutionException occurs when the given file cannot be opened or is a valid XML file.
386      */
387     private Document parseFile( File file, DocumentBuilder constructor ) throws MojoExecutionException
388     {
389         if ( constructor == null )
390         {
391             return null;
392         }
393         // The document is the root of the DOM tree.
394         File targetFile = file.getAbsoluteFile();
395         getLog().info( "Parsing " + targetFile );
396         Document doc = null;
397         try
398         {
399             doc = constructor.parse( targetFile );
400         }
401         catch ( SAXException e )
402         {
403             getLog().error( "Cannot parse " + targetFile + " : " + e.getMessage() );
404             throw new MojoExecutionException( "Cannot parse " + targetFile + " : " + e.getMessage() );
405         }
406         catch ( IOException e )
407         {
408             getLog().error( "Cannot open " + targetFile + " : " + e.getMessage() );
409             throw new MojoExecutionException( "Cannot open " + targetFile + " : " + e.getMessage() );
410         }
411         return doc;
412     }
413 
414 
415     /**
416      * write a Node in a xml file.
417      *
418      * @param outputFilename URI to the output file
419      * @param treeToBeWrite Node root of the tree to be write in file
420      * @throws MojoExecutionException if the plugin failed
421      */
422     private void writeToFile( URI outputFilename, Node treeToBeWrite ) throws MojoExecutionException
423     {
424         // init the transformer
425         Transformer transformer = null;
426         TransformerFactory tfabrique = TransformerFactory.newInstance();
427         try
428         {
429             transformer = tfabrique.newTransformer();
430         }
431         catch ( TransformerConfigurationException e )
432         {
433             getLog().error( "Unable to write to file: " + outputFilename.toString() );
434             throw new MojoExecutionException( "Unable to write to file: " + outputFilename.toString() + " : "
435                 + e.getMessage() );
436         }
437         Properties proprietes = new Properties();
438         proprietes.put( "method", "xml" );
439         proprietes.put( "version", "1.0" );
440         proprietes.put( "encoding", "ISO-8859-1" );
441         proprietes.put( "standalone", "yes" );
442         proprietes.put( "indent", "yes" );
443         proprietes.put( "omit-xml-declaration", "no" );
444         transformer.setOutputProperties( proprietes );
445 
446         DOMSource input = new DOMSource( treeToBeWrite );
447 
448         File fichier = new File( outputFilename );
449         FileOutputStream flux = null;
450         try
451         {
452             flux = new FileOutputStream( fichier );
453         }
454         catch ( FileNotFoundException e )
455         {
456             getLog().error( "Unable to write to file: " + fichier.getName() );
457             throw new MojoExecutionException( "Unable to write to file: " + fichier.getName() + " : " + e.getMessage() );
458         }
459         Result output = new StreamResult( flux );
460         try
461         {
462             transformer.transform( input, output );
463         }
464         catch ( TransformerException e )
465         {
466             throw new MojoExecutionException( "Unable to write to file: " + outputFilename.toString() + " : "
467                 + e.getMessage() );
468         }
469 
470         try
471         {
472             flux.flush();
473             flux.close();
474         }
475         catch ( IOException e )
476         {
477             throw new MojoExecutionException( "IOException when closing file : " + e.getMessage() );
478         }
479     }
480 }