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