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 * @throws IOException If a file/directory could not be deleted and <code>failOnError</code> is <code>true</code>.
91 */
92 public void delete( File basedir, Selector selector, boolean followSymlinks, boolean failOnError )
93 throws IOException
94 {
95 if ( !basedir.isDirectory() )
96 {
97 if ( !basedir.exists() )
98 {
99 if ( logDebug != null )
100 {
101 logDebug.log( "Skipping non-existing directory " + basedir );
102 }
103 return;
104 }
105 throw new IOException( "Invalid base directory " + basedir );
106 }
107
108 if ( logInfo != null )
109 {
110 logInfo.log( "Deleting " + basedir + ( selector != null ? " (" + selector + ")" : "" ) );
111 }
112
113 File file = followSymlinks ? basedir : basedir.getCanonicalFile();
114
115 delete( file, "", selector, followSymlinks, failOnError );
116 }
117
118 /**
119 * Deletes the specified file or directory.
120 *
121 * @param file The file/directory to delete, must not be <code>null</code>. If <code>followSymlinks</code> is
122 * <code>false</code>, it is assumed that the parent file is canonical.
123 * @param pathname The relative pathname of the file, using {@link File#separatorChar}, must not be
124 * <code>null</code>.
125 * @param selector The selector used to determine what contents to delete, may be <code>null</code> to delete
126 * everything.
127 * @param followSymlinks Whether to follow symlinks.
128 * @param failOnError Whether to abort with an exception in case a selected file/directory could not be deleted.
129 * @return The result of the cleaning, never <code>null</code>.
130 * @throws IOException If a file/directory could not be deleted and <code>failOnError</code> is <code>true</code>.
131 */
132 private Result delete( File file, String pathname, Selector selector, boolean followSymlinks, boolean failOnError )
133 throws IOException
134 {
135 Result result = new Result();
136
137 boolean isDirectory = file.isDirectory();
138
139 if ( isDirectory )
140 {
141 if ( selector == null || selector.couldHoldSelected( pathname ) )
142 {
143 File canonical = followSymlinks ? file : file.getCanonicalFile();
144 if ( followSymlinks || file.equals( canonical ) )
145 {
146 String[] filenames = canonical.list();
147 if ( filenames != null )
148 {
149 String prefix = ( pathname.length() > 0 ) ? pathname + File.separatorChar : "";
150 for ( int i = filenames.length - 1; i >= 0; i-- )
151 {
152 String filename = filenames[i];
153 File child = new File( canonical, filename );
154 result.update( delete( child, prefix + filename, selector, followSymlinks, failOnError ) );
155 }
156 }
157 }
158 else if ( logDebug != null )
159 {
160 logDebug.log( "Not recursing into symlink " + file );
161 }
162 }
163 else if ( logDebug != null )
164 {
165 logDebug.log( "Not recursing into directory without included files " + file );
166 }
167 }
168
169 if ( !result.excluded && ( selector == null || selector.isSelected( pathname ) ) )
170 {
171 if ( logVerbose != null )
172 {
173 if ( isDirectory )
174 {
175 logVerbose.log( "Deleting directory " + file );
176 }
177 else if ( file.exists() )
178 {
179 logVerbose.log( "Deleting file " + file );
180 }
181 else
182 {
183 logVerbose.log( "Deleting dangling symlink " + file );
184 }
185 }
186 result.failures += delete( file, failOnError );
187 }
188 else
189 {
190 result.excluded = true;
191 }
192
193 return result;
194 }
195
196 /**
197 * Deletes the specified file, directory. If the path denotes a symlink, only the link is removed, its target is
198 * left untouched.
199 *
200 * @param file The file/directory to delete, must not be <code>null</code>.
201 * @param failOnError Whether to abort with an exception in case the file/directory could not be deleted.
202 * @return <code>0</code> if the file was deleted, <code>1</code> otherwise.
203 * @throws IOException If a file/directory could not be deleted and <code>failOnError</code> is <code>true</code>.
204 */
205 private int delete( File file, boolean failOnError )
206 throws IOException
207 {
208 if ( !file.delete() )
209 {
210 if ( ON_WINDOWS )
211 {
212 // try to release any locks held by non-closed files
213 System.gc();
214 }
215 try
216 {
217 Thread.sleep( 10 );
218 }
219 catch ( InterruptedException e )
220 {
221 // ignore
222 }
223 if ( !file.delete() )
224 {
225 if ( failOnError )
226 {
227 throw new IOException( "Failed to delete " + file );
228 }
229 else
230 {
231 if ( logWarn != null )
232 {
233 logWarn.log( "Failed to delete " + file );
234 }
235 return 1;
236 }
237 }
238 }
239
240 return 0;
241 }
242
243 private static class Result
244 {
245
246 public int failures;
247
248 public boolean excluded;
249
250 public void update( Result result )
251 {
252 failures += result.failures;
253 excluded |= result.excluded;
254 }
255
256 }
257
258 private static interface Logger
259 {
260
261 public void log( CharSequence message );
262
263 }
264
265 }