View Javadoc
1   package org.apache.maven.plugin.clean;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.io.IOException;
24  
25  import org.apache.maven.plugin.logging.Log;
26  import org.apache.maven.shared.utils.Os;
27  import org.apache.maven.shared.utils.io.FileUtils;
28  
29  /**
30   * Cleans directories.
31   * 
32   * @author Benjamin Bentmann
33   */
34  class Cleaner
35  {
36  
37      private static final boolean ON_WINDOWS = Os.isFamily( Os.FAMILY_WINDOWS );
38  
39      private final Logger logDebug;
40  
41      private final Logger logInfo;
42  
43      private final Logger logVerbose;
44  
45      private final Logger logWarn;
46  
47      /**
48       * Creates a new cleaner.
49       * 
50       * @param log The logger to use, may be <code>null</code> to disable logging.
51       * @param verbose Whether to perform verbose logging.
52       */
53      public Cleaner( final Log log, boolean verbose )
54      {
55          logDebug = ( log == null || !log.isDebugEnabled() ) ? null : new Logger()
56          {
57              public void log( CharSequence message )
58              {
59                  log.debug( message );
60              }
61          };
62  
63          logInfo = ( log == null || !log.isInfoEnabled() ) ? null : new Logger()
64          {
65              public void log( CharSequence message )
66              {
67                  log.info( message );
68              }
69          };
70  
71          logWarn = ( log == null || !log.isWarnEnabled() ) ? null : new Logger()
72          {
73              public void log( CharSequence message )
74              {
75                  log.warn( message );
76              }
77          };
78  
79          logVerbose = verbose ? logInfo : logDebug;
80      }
81  
82      /**
83       * Deletes the specified directories and its contents.
84       * 
85       * @param basedir The directory to delete, must not be <code>null</code>. Non-existing directories will be silently
86       *            ignored.
87       * @param selector The selector used to determine what contents to delete, may be <code>null</code> to delete
88       *            everything.
89       * @param followSymlinks Whether to follow symlinks.
90       * @param failOnError Whether to abort with an exception in case a selected file/directory could not be deleted.
91       * @param retryOnError Whether to undertake additional delete attempts in case the first attempt failed.
92       * @throws IOException If a file/directory could not be deleted and <code>failOnError</code> is <code>true</code>.
93       */
94      public void delete( File basedir, Selector selector, boolean followSymlinks, boolean failOnError,
95                          boolean retryOnError )
96          throws IOException
97      {
98          if ( !basedir.isDirectory() )
99          {
100             if ( !basedir.exists() )
101             {
102                 if ( logDebug != null )
103                 {
104                     logDebug.log( "Skipping non-existing directory " + basedir );
105                 }
106                 return;
107             }
108             throw new IOException( "Invalid base directory " + basedir );
109         }
110 
111         if ( logInfo != null )
112         {
113             logInfo.log( "Deleting " + basedir + ( selector != null ? " (" + selector + ")" : "" ) );
114         }
115 
116         File file = followSymlinks ? basedir : basedir.getCanonicalFile();
117 
118         delete( file, "", selector, followSymlinks, failOnError, retryOnError );
119     }
120 
121     /**
122      * Deletes the specified file or directory.
123      * 
124      * @param file The file/directory to delete, must not be <code>null</code>. If <code>followSymlinks</code> is
125      *            <code>false</code>, it is assumed that the parent file is canonical.
126      * @param pathname The relative pathname of the file, using {@link File#separatorChar}, must not be
127      *            <code>null</code>.
128      * @param selector The selector used to determine what contents to delete, may be <code>null</code> to delete
129      *            everything.
130      * @param followSymlinks Whether to follow symlinks.
131      * @param failOnError Whether to abort with an exception in case a selected file/directory could not be deleted.
132      * @param retryOnError Whether to undertake additional delete attempts in case the first attempt failed.
133      * @return The result of the cleaning, never <code>null</code>.
134      * @throws IOException If a file/directory could not be deleted and <code>failOnError</code> is <code>true</code>.
135      */
136     private Result delete( File file, String pathname, Selector selector, boolean followSymlinks, boolean failOnError,
137                            boolean retryOnError )
138         throws IOException
139     {
140         Result result = new Result();
141 
142         boolean isDirectory = file.isDirectory();
143 
144         if ( isDirectory )
145         {
146             if ( selector == null || selector.couldHoldSelected( pathname ) )
147             {
148                 final boolean isSymlink = FileUtils.isSymbolicLink( file );
149                 File canonical = followSymlinks ? file : file.getCanonicalFile();
150                 if ( followSymlinks || !isSymlink )
151                 {
152                     String[] filenames = canonical.list();
153                     if ( filenames != null )
154                     {
155                         String prefix = ( pathname.length() > 0 ) ? pathname + File.separatorChar : "";
156                         for ( int i = filenames.length - 1; i >= 0; i-- )
157                         {
158                             String filename = filenames[i];
159                             File child = new File( canonical, filename );
160                             result.update( delete( child, prefix + filename, selector, followSymlinks, failOnError,
161                                                    retryOnError ) );
162                         }
163                     }
164                 }
165                 else if ( logDebug != null )
166                 {
167                     logDebug.log( "Not recursing into symlink " + file );
168                 }
169             }
170             else if ( logDebug != null )
171             {
172                 logDebug.log( "Not recursing into directory without included files " + file );
173             }
174         }
175 
176         if ( !result.excluded && ( selector == null || selector.isSelected( pathname ) ) )
177         {
178             if ( logVerbose != null )
179             {
180                 if ( isDirectory )
181                 {
182                     logVerbose.log( "Deleting directory " + file );
183                 }
184                 else if ( file.exists() )
185                 {
186                     logVerbose.log( "Deleting file " + file );
187                 }
188                 else
189                 {
190                     logVerbose.log( "Deleting dangling symlink " + file );
191                 }
192             }
193             result.failures += delete( file, failOnError, retryOnError );
194         }
195         else
196         {
197             result.excluded = true;
198         }
199 
200         return result;
201     }
202 
203     /**
204      * Deletes the specified file, directory. If the path denotes a symlink, only the link is removed, its target is
205      * left untouched.
206      * 
207      * @param file The file/directory to delete, must not be <code>null</code>.
208      * @param failOnError Whether to abort with an exception in case the file/directory could not be deleted.
209      * @param retryOnError Whether to undertake additional delete attempts in case the first attempt failed.
210      * @return <code>0</code> if the file was deleted, <code>1</code> otherwise.
211      * @throws IOException If a file/directory could not be deleted and <code>failOnError</code> is <code>true</code>.
212      */
213     private int delete( File file, boolean failOnError, boolean retryOnError )
214         throws IOException
215     {
216         if ( !file.delete() )
217         {
218             boolean deleted = false;
219 
220             if ( retryOnError )
221             {
222                 if ( ON_WINDOWS )
223                 {
224                     // try to release any locks held by non-closed files
225                     System.gc();
226                 }
227 
228                 final int[] delays = { 50, 250, 750 };
229                 for ( int i = 0; !deleted && i < delays.length; i++ )
230                 {
231                     try
232                     {
233                         Thread.sleep( delays[i] );
234                     }
235                     catch ( InterruptedException e )
236                     {
237                         // ignore
238                     }
239                     deleted = file.delete() || !file.exists();
240                 }
241             }
242             else
243             {
244                 deleted = !file.exists();
245             }
246 
247             if ( !deleted )
248             {
249                 if ( failOnError )
250                 {
251                     throw new IOException( "Failed to delete " + file );
252                 }
253                 else
254                 {
255                     if ( logWarn != null )
256                     {
257                         logWarn.log( "Failed to delete " + file );
258                     }
259                     return 1;
260                 }
261             }
262         }
263 
264         return 0;
265     }
266 
267     private static class Result
268     {
269 
270         private int failures;
271 
272         private boolean excluded;
273 
274         public void update( Result result )
275         {
276             failures += result.failures;
277             excluded |= result.excluded;
278         }
279 
280     }
281 
282     private interface Logger
283     {
284 
285         void log( CharSequence message );
286 
287     }
288 
289 }