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