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