1 package org.apache.maven.plugins.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 }