25
25
26
26
import java .io .File ;
27
27
import java .io .IOException ;
28
+ import java .nio .file .DirectoryStream ;
28
29
import java .nio .file .Files ;
30
+ import java .nio .file .Path ;
29
31
import java .nio .file .Paths ;
30
32
import java .nio .file .StandardCopyOption ;
33
+ import java .util .ArrayList ;
34
+ import java .util .Comparator ;
31
35
import java .util .HashSet ;
32
36
import java .util .List ;
33
37
import java .util .Map ;
34
38
import java .util .Set ;
39
+ import java .util .TreeSet ;
35
40
import java .util .logging .Level ;
36
41
import java .util .logging .Logger ;
37
42
import java .util .stream .Collectors ;
@@ -53,6 +58,25 @@ public class PendingFileCompleter {
53
58
private static final Logger LOGGER =
54
59
LoggerFactory .getLogger (PendingFileCompleter .class );
55
60
61
+ /**
62
+ * Descending path segment length comparator
63
+ */
64
+ private static final Comparator <File > DESC_PATHLEN_COMPARATOR =
65
+ (File f1 , File f2 ) -> {
66
+ String s1 = f1 .getAbsolutePath ();
67
+ String s2 = f2 .getAbsolutePath ();
68
+ int n1 = countPathSegments (s1 );
69
+ int n2 = countPathSegments (s2 );
70
+ // DESC: s2 no. of segments <=> s1 no. of segments
71
+ int cmp = Integer .compare (n2 , n1 );
72
+ if (cmp != 0 ) return cmp ;
73
+
74
+ // the Comparator must also be "consistent with equals", so check
75
+ // string contents too when (length)cmp == 0. (ASC: s1 <=> s2.)
76
+ cmp = s1 .compareTo (s2 );
77
+ return cmp ;
78
+ };
79
+
56
80
private final Set <PendingFileDeletion > deletions = new HashSet <>();
57
81
58
82
private final Set <PendingFileRenaming > renames = new HashSet <>();
@@ -125,6 +149,8 @@ private int completeRenamings() throws IOException {
125
149
int numPending = renames .size ();
126
150
int numFailures = 0 ;
127
151
152
+ if (numPending < 1 ) return 0 ;
153
+
128
154
List <PendingFileRenamingExec > pendingExecs = renames .
129
155
parallelStream ().map (f ->
130
156
new PendingFileRenamingExec (f .getTransientPath (),
@@ -168,6 +194,8 @@ private int completeDeletions() throws IOException {
168
194
int numPending = deletions .size ();
169
195
int numFailures = 0 ;
170
196
197
+ if (numPending < 1 ) return 0 ;
198
+
171
199
List <PendingFileDeletionExec > pendingExecs = deletions .
172
200
parallelStream ().map (f ->
173
201
new PendingFileDeletionExec (f .getAbsolutePath ())).collect (
@@ -186,6 +214,10 @@ private int completeDeletions() throws IOException {
186
214
}));
187
215
deletions .clear ();
188
216
217
+ List <PendingFileDeletionExec > successes = bySuccess .getOrDefault (
218
+ Boolean .TRUE , null );
219
+ if (successes != null ) tryDeleteParents (successes );
220
+
189
221
List <PendingFileDeletionExec > failures = bySuccess .getOrDefault (
190
222
Boolean .FALSE , null );
191
223
if (failures != null && failures .size () > 0 ) {
@@ -202,15 +234,12 @@ private int completeDeletions() throws IOException {
202
234
203
235
private void doDelete (PendingFileDeletionExec del ) throws IOException {
204
236
File f = new File (del .absolutePath + PENDING_EXTENSION );
237
+ File parent = f .getParentFile ();
238
+ del .absoluteParent = parent ;
239
+
205
240
doDelete (f );
206
241
f = new File (del .absolutePath );
207
242
doDelete (f );
208
-
209
- File parent = f .getParentFile ();
210
- if (parent .delete ()) {
211
- LOGGER .log (Level .FINE , "Removed empty parent dir: {0}" ,
212
- parent .getAbsolutePath ());
213
- }
214
243
}
215
244
216
245
private void doDelete (File f ) {
@@ -237,8 +266,111 @@ private void doRename(PendingFileRenamingExec ren) throws IOException {
237
266
}
238
267
}
239
268
269
+ private void tryDeleteParents (List <PendingFileDeletionExec > dels ) {
270
+ Set <File > parents = new TreeSet <>(DESC_PATHLEN_COMPARATOR );
271
+ dels .forEach ((del ) -> { parents .add (del .absoluteParent ); });
272
+
273
+ for (File dir : parents ) {
274
+ Set <File > children = findFilelessChildren (dir );
275
+ children .forEach ((childDir ) -> {
276
+ tryDeleteDirectory (childDir );
277
+ });
278
+
279
+ tryDeleteDirectory (dir );
280
+ }
281
+ }
282
+
283
+ private void tryDeleteDirectory (File dir ) {
284
+ if (dir .delete ()) {
285
+ LOGGER .log (Level .FINE , "Removed empty parent dir: {0}" ,
286
+ dir .getAbsolutePath ());
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Determines a DESC ordered set of eligible file-less child directories
292
+ * for cleaning up.
293
+ */
294
+ private Set <File > findFilelessChildren (File directory ) {
295
+ SkeletonDirs ret = new SkeletonDirs ();
296
+ findFilelessChildrenDeep (ret , directory );
297
+
298
+ Set <File > parents = new TreeSet <>(DESC_PATHLEN_COMPARATOR );
299
+ // N.b. the `ineligible' field is not relevant here but only during
300
+ // recursion. `childDirs' contains eligible directories.
301
+ parents .addAll (ret .childDirs );
302
+ return parents ;
303
+ }
304
+
305
+ /**
306
+ * Recursive method used by {@link #findFilelessChildren(java.io.File)}.
307
+ */
308
+ private void findFilelessChildrenDeep (SkeletonDirs ret , File directory ) {
309
+
310
+ if (!directory .exists ()) return ;
311
+ String dirPath = directory .getAbsolutePath ();
312
+ boolean topLevelIneligible = false ;
313
+ boolean didLogFileTopLevelIneligible = false ;
314
+
315
+ try {
316
+ DirectoryStream <Path > directoryStream = Files .newDirectoryStream (
317
+ Paths .get (dirPath ));
318
+ for (Path path : directoryStream ) {
319
+ File f = path .toFile ();
320
+ if (f .isFile ()) {
321
+ topLevelIneligible = true ;
322
+ if (!didLogFileTopLevelIneligible && LOGGER .isLoggable (
323
+ Level .FINEST )) {
324
+ didLogFileTopLevelIneligible = true ; // just once is OK
325
+ LOGGER .log (Level .FINEST , "not file-less due to: {0}" ,
326
+ f .getAbsolutePath ());
327
+ }
328
+ } else {
329
+ findFilelessChildrenDeep (ret , f );
330
+ if (!ret .ineligible ) {
331
+ ret .childDirs .add (f );
332
+ } else {
333
+ topLevelIneligible = true ;
334
+ if (LOGGER .isLoggable (Level .FINEST )) {
335
+ LOGGER .log (Level .FINEST ,
336
+ "its children prevent delete: {0}" ,
337
+ f .getAbsolutePath ());
338
+ }
339
+ }
340
+
341
+ // Reset this flag so that other potential, eligible
342
+ // children are evaluated.
343
+ ret .ineligible = false ;
344
+ }
345
+ }
346
+ } catch (IOException ex ) {
347
+ topLevelIneligible = true ;
348
+ if (LOGGER .isLoggable (Level .FINEST )) {
349
+ LOGGER .log (Level .FINEST , "Failed to stream directory:" +
350
+ directory , ex );
351
+ }
352
+ }
353
+
354
+ ret .ineligible = topLevelIneligible ;
355
+ }
356
+
357
+ /**
358
+ * Counts segments arising from {@code File.separatorChar} or '\\'
359
+ * @param path
360
+ * @return a natural number
361
+ */
362
+ private static int countPathSegments (String path ) {
363
+ int n = 1 ;
364
+ for (int i = 0 ; i < path .length (); ++i ) {
365
+ char c = path .charAt (i );
366
+ if (c == File .separatorChar || c == '\\' ) ++n ;
367
+ }
368
+ return n ;
369
+ }
370
+
240
371
private class PendingFileDeletionExec {
241
372
public String absolutePath ;
373
+ public File absoluteParent ;
242
374
public IOException exception ;
243
375
public PendingFileDeletionExec (String absolutePath ) {
244
376
this .absolutePath = absolutePath ;
@@ -254,4 +386,13 @@ public PendingFileRenamingExec(String source, String target) {
254
386
this .target = target ;
255
387
}
256
388
}
389
+
390
+ /**
391
+ * Represents a collection of file-less directories which should also be
392
+ * deleted for cleanliness.
393
+ */
394
+ private class SkeletonDirs {
395
+ public boolean ineligible ; // a flag used during recursion
396
+ public List <File > childDirs = new ArrayList <>();
397
+ }
257
398
}
0 commit comments