OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 180 | Go to most recent revision | Only display areas with differences | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 180 Rev 182
1
/*
1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 * 
3
 * 
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
5
 * 
5
 * 
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
9
 * language governing permissions and limitations under the License.
10
 * 
10
 * 
11
 * When distributing the software, include this License Header Notice in each file.
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
12
 */
13
 
13
 
14
 package org.openconcerto.utils;
14
 package org.openconcerto.utils;
15
 
15
 
16
import org.openconcerto.utils.CollectionMap2.Mode;
16
import org.openconcerto.utils.CollectionMap2.Mode;
17
import org.openconcerto.utils.DesktopEnvironment.Gnome;
17
import org.openconcerto.utils.DesktopEnvironment.Gnome;
18
import org.openconcerto.utils.OSFamily.Unix;
18
import org.openconcerto.utils.OSFamily.Unix;
19
import org.openconcerto.utils.StringUtils.Escaper;
19
import org.openconcerto.utils.StringUtils.Escaper;
20
import org.openconcerto.utils.cc.ExnTransformer;
20
import org.openconcerto.utils.cc.ExnTransformer;
21
import org.openconcerto.utils.cc.IClosure;
21
import org.openconcerto.utils.cc.IClosure;
22
 
22
 
23
import java.awt.Desktop;
23
import java.awt.Desktop;
24
import java.io.BufferedReader;
24
import java.io.BufferedReader;
25
import java.io.BufferedWriter;
25
import java.io.BufferedWriter;
26
import java.io.File;
26
import java.io.File;
27
import java.io.FileFilter;
27
import java.io.FileFilter;
28
import java.io.FileInputStream;
28
import java.io.FileInputStream;
29
import java.io.FileNotFoundException;
29
import java.io.FileNotFoundException;
30
import java.io.FileOutputStream;
30
import java.io.FileOutputStream;
31
import java.io.IOException;
31
import java.io.IOException;
32
import java.io.InputStream;
32
import java.io.InputStream;
33
import java.io.InputStreamReader;
33
import java.io.InputStreamReader;
34
import java.io.OutputStreamWriter;
34
import java.io.OutputStreamWriter;
35
import java.io.RandomAccessFile;
35
import java.io.RandomAccessFile;
36
import java.io.Reader;
36
import java.io.Reader;
37
import java.net.URI;
37
import java.net.URI;
38
import java.net.URL;
38
import java.net.URL;
39
import java.nio.channels.FileChannel;
39
import java.nio.channels.FileChannel;
40
import java.nio.charset.Charset;
40
import java.nio.charset.Charset;
41
import java.nio.charset.StandardCharsets;
41
import java.nio.charset.StandardCharsets;
42
import java.nio.file.CopyOption;
42
import java.nio.file.CopyOption;
43
import java.nio.file.DirectoryStream;
43
import java.nio.file.DirectoryStream;
44
import java.nio.file.DirectoryStream.Filter;
44
import java.nio.file.DirectoryStream.Filter;
45
import java.nio.file.FileVisitResult;
45
import java.nio.file.FileVisitResult;
46
import java.nio.file.Files;
46
import java.nio.file.Files;
47
import java.nio.file.LinkOption;
47
import java.nio.file.LinkOption;
48
import java.nio.file.OpenOption;
48
import java.nio.file.OpenOption;
49
import java.nio.file.Path;
49
import java.nio.file.Path;
50
import java.nio.file.SimpleFileVisitor;
50
import java.nio.file.SimpleFileVisitor;
51
import java.nio.file.StandardCopyOption;
51
import java.nio.file.StandardCopyOption;
52
import java.nio.file.StandardOpenOption;
52
import java.nio.file.StandardOpenOption;
53
import java.nio.file.attribute.BasicFileAttributes;
53
import java.nio.file.attribute.BasicFileAttributes;
54
import java.nio.file.attribute.PosixFileAttributeView;
54
import java.nio.file.attribute.PosixFileAttributeView;
55
import java.nio.file.attribute.PosixFilePermissions;
55
import java.nio.file.attribute.PosixFilePermissions;
56
import java.util.ArrayList;
56
import java.util.ArrayList;
57
import java.util.Arrays;
57
import java.util.Arrays;
58
import java.util.Collection;
58
import java.util.Collection;
59
import java.util.Collections;
59
import java.util.Collections;
60
import java.util.EnumSet;
60
import java.util.EnumSet;
61
import java.util.HashMap;
61
import java.util.HashMap;
62
import java.util.HashSet;
62
import java.util.HashSet;
63
import java.util.List;
63
import java.util.List;
64
import java.util.Map;
64
import java.util.Map;
65
import java.util.Map.Entry;
65
import java.util.Map.Entry;
66
import java.util.Objects;
66
import java.util.Objects;
67
import java.util.Set;
67
import java.util.Set;
68
import java.util.logging.Level;
68
import java.util.logging.Level;
69
import java.util.regex.Pattern;
69
import java.util.regex.Pattern;
70
 
70
 
71
public final class FileUtils {
71
public final class FileUtils {
72
 
72
 
73
    public static void main(String[] args) throws Exception {
73
    public static void main(String[] args) throws Exception {
74
        final String cmd = args[0];
74
        final String cmd = args[0];
75
        if ("browseFile".equals(cmd))
75
        if ("browseFile".equals(cmd))
76
            browseFile(new File(args[1]));
76
            browseFile(new File(args[1]));
77
        else if ("browse".equals(cmd))
77
        else if ("browse".equals(cmd))
78
            browse(new URI(args[1]));
78
            browse(new URI(args[1]));
79
        else
79
        else
80
            System.err.println("Unkown command : " + cmd);
80
            System.err.println("Unkown command : " + cmd);
81
    }
81
    }
82
 
82
 
83
    private FileUtils() {
83
    private FileUtils() {
84
        // all static
84
        // all static
85
    }
85
    }
86
 
86
 
87
    public static void browseFile(final File f) throws IOException {
87
    public static void browseFile(final File f) throws IOException {
88
        browse(null, Objects.requireNonNull(f));
88
        browse(null, Objects.requireNonNull(f));
89
    }
89
    }
90
 
90
 
91
    private static void browse(URI uri, final File f) throws IOException {
91
    private static void browse(URI uri, final File f) throws IOException {
92
        assert (uri == null) != (f == null);
92
        assert (uri == null) != (f == null);
93
        boolean handled = false;
93
        boolean handled = false;
94
        final Desktop.Action action = Desktop.Action.BROWSE;
94
        final Desktop.Action action = Desktop.Action.BROWSE;
95
        if (isDesktopDesirable(action) && Desktop.isDesktopSupported()) {
95
        if (isDesktopDesirable(action) && Desktop.isDesktopSupported()) {
96
            final Desktop d = Desktop.getDesktop();
96
            final Desktop d = Desktop.getDesktop();
97
            if (d.isSupported(action)) {
97
            if (d.isSupported(action)) {
98
                if (uri == null)
98
                if (uri == null)
99
                    uri = f.getCanonicalFile().toURI();
99
                    uri = f.getCanonicalFile().toURI();
100
                if (!uri.getScheme().equals("file") && DesktopEnvironment.getDE() instanceof Gnome) {
100
                if (!uri.getScheme().equals("file") && DesktopEnvironment.getDE() instanceof Gnome) {
101
                    ProcessBuilder pb = null;
101
                    ProcessBuilder pb = null;
102
                    final String version = DesktopEnvironment.getDE().getVersion();
102
                    final String version = DesktopEnvironment.getDE().getVersion();
103
                    // Ubuntu 12.04, 14.04, 16.04
103
                    // Ubuntu 12.04, 14.04, 16.04
104
                    if (version.startsWith("3.4.") || version.startsWith("3.10.") || version.startsWith("3.18.")) {
104
                    if (version.startsWith("3.4.") || version.startsWith("3.10.") || version.startsWith("3.18.")) {
105
                        pb = new ProcessBuilder("gvfs-mount", uri.toASCIIString());
105
                        pb = new ProcessBuilder("gvfs-mount", uri.toASCIIString());
106
                        // Ubuntu 18.04
106
                        // Ubuntu 18.04
107
                    } else if (version.startsWith("3.28")) {
107
                    } else if (version.startsWith("3.28")) {
108
                        // gio/gio-tool-mount.c#mount() calls g_file_mount_enclosing_volume.
108
                        // gio/gio-tool-mount.c#mount() calls g_file_mount_enclosing_volume.
109
                        // TODO find out how glib computes "the volume that contains the file
109
                        // TODO find out how glib computes "the volume that contains the file
110
                        // location". E.g why does mount "davs://example.com/webdav/dir/dir" mounts
110
                        // location". E.g why does mount "davs://example.com/webdav/dir/dir" mounts
111
                        // "davs://example.com/webdav/".
111
                        // "davs://example.com/webdav/".
112
                        // Return 0 if not yet mounted, 2 if it was already mounted.
112
                        // Return 0 if not yet mounted, 2 if it was already mounted.
113
                        pb = new ProcessBuilder("gio", "mount", uri.toASCIIString());
113
                        pb = new ProcessBuilder("gio", "mount", uri.toASCIIString());
114
                    }
114
                    }
115
                    if (pb != null) {
115
                    if (pb != null) {
116
                        try {
116
                        try {
117
                            startDiscardingOutput(pb).waitFor();
117
                            startDiscardingOutput(pb).waitFor();
118
                        } catch (InterruptedException e) {
118
                        } catch (InterruptedException e) {
119
                            throw new RTInterruptedException("Interrupted while waiting on mount for " + uri, e);
119
                            throw new RTInterruptedException("Interrupted while waiting on mount for " + uri, e);
120
                        }
120
                        }
121
                    }
121
                    }
122
                }
122
                }
123
                d.browse(uri);
123
                d.browse(uri);
124
                handled = true;
124
                handled = true;
125
            }
125
            }
126
        }
126
        }
127
        if (!handled) {
127
        if (!handled) {
128
            // if the caller passed a file use it instead of our converted URI
128
            // if the caller passed a file use it instead of our converted URI
129
            if (f != null)
129
            if (f != null)
130
                openNative(f);
130
                openNative(f);
131
            else
131
            else
132
                openNative(uri);
132
                openNative(uri);
133
        }
133
        }
134
    }
134
    }
135
 
135
 
136
    public static boolean isDesktopDesirable(Desktop.Action action) {
136
    public static boolean isDesktopDesirable(Desktop.Action action) {
137
        // apparently the JRE just checks if gnome libs are available (e.g. open Nautilus in XFCE)
137
        // apparently the JRE just checks if gnome libs are available (e.g. open Nautilus in XFCE)
138
        return !(action == Desktop.Action.BROWSE && OSFamily.getInstance() == OSFamily.Linux && !(DesktopEnvironment.getDE() instanceof Gnome));
138
        return !(action == Desktop.Action.BROWSE && OSFamily.getInstance() == OSFamily.Linux && !(DesktopEnvironment.getDE() instanceof Gnome));
139
    }
139
    }
140
 
140
 
141
    public static void browse(URI uri) throws IOException {
141
    public static void browse(URI uri) throws IOException {
142
        browse(Objects.requireNonNull(uri), null);
142
        browse(Objects.requireNonNull(uri), null);
143
    }
143
    }
144
 
144
 
145
    public static void openFile(File f) throws IOException {
145
    public static void openFile(File f) throws IOException {
146
        if (!f.exists()) {
146
        if (!f.exists()) {
147
            throw new FileNotFoundException(f.getAbsolutePath() + " not found");
147
            throw new FileNotFoundException(f.getAbsolutePath() + " not found");
148
        }
148
        }
149
        if (Desktop.isDesktopSupported()) {
149
        if (Desktop.isDesktopSupported()) {
150
            Desktop d = Desktop.getDesktop();
150
            Desktop d = Desktop.getDesktop();
151
            if (d.isSupported(Desktop.Action.OPEN)) {
151
            if (d.isSupported(Desktop.Action.OPEN)) {
152
                d.open(f.getCanonicalFile());
152
                d.open(f.getCanonicalFile());
153
            } else {
153
            } else {
154
                openNative(f);
154
                openNative(f);
155
            }
155
            }
156
        } else {
156
        } else {
157
            openNative(f);
157
            openNative(f);
158
        }
158
        }
159
    }
159
    }
160
 
160
 
161
    // immutable, getAbsoluteFile() required otherwise list() returns null
161
    // immutable, getAbsoluteFile() required otherwise list() returns null
162
    static private final File WD = new File("").getAbsoluteFile();
162
    static private final File WD = new File("").getAbsoluteFile();
163
 
163
 
164
    static public final File getWD() {
164
    static public final File getWD() {
165
        return WD;
165
        return WD;
166
    }
166
    }
167
 
167
 
168
    /**
168
    /**
169
     * All the files (see {@link File#isFile()}) contained in the passed dir.
169
     * All the files (see {@link File#isFile()}) contained in the passed dir.
170
     * 
170
     * 
171
     * @param dir the root directory to search.
171
     * @param dir the root directory to search.
172
     * @return a List of String.
172
     * @return a List of String.
173
     */
173
     */
174
    public static List<String> listR(File dir) {
174
    public static List<String> listR(File dir) {
175
        return listR(dir, REGULAR_FILE_FILTER);
175
        return listR(dir, REGULAR_FILE_FILTER);
176
    }
176
    }
177
 
177
 
178
    public static List<String> listR(File dir, FileFilter ff) {
178
    public static List<String> listR(File dir, FileFilter ff) {
179
        return listR_rec(dir, ff, ".");
179
        return listR_rec(dir, ff, ".");
180
    }
180
    }
181
 
181
 
182
    private static List<String> listR_rec(File dir, FileFilter ff, String prefix) {
182
    private static List<String> listR_rec(File dir, FileFilter ff, String prefix) {
183
        if (!dir.isDirectory())
183
        if (!dir.isDirectory())
184
            return null;
184
            return null;
185
 
185
 
186
        final List<String> res = new ArrayList<String>();
186
        final List<String> res = new ArrayList<String>();
187
        final File[] children = dir.listFiles();
187
        final File[] children = dir.listFiles();
188
        for (int i = 0; i < children.length; i++) {
188
        for (int i = 0; i < children.length; i++) {
189
            final String newPrefix = prefix + "/" + children[i].getName();
189
            final String newPrefix = prefix + "/" + children[i].getName();
190
            if (ff == null || ff.accept(children[i])) {
190
            if (ff == null || ff.accept(children[i])) {
191
                res.add(newPrefix);
191
                res.add(newPrefix);
192
            }
192
            }
193
            if (children[i].isDirectory()) {
193
            if (children[i].isDirectory()) {
194
                res.addAll(listR_rec(children[i], ff, newPrefix));
194
                res.addAll(listR_rec(children[i], ff, newPrefix));
195
            }
195
            }
196
        }
196
        }
197
        return res;
197
        return res;
198
    }
198
    }
199
 
199
 
200
    public static void walk(File dir, IClosure<File> c) {
200
    public static void walk(File dir, IClosure<File> c) {
201
        walk(dir, c, RecursionType.BREADTH_FIRST);
201
        walk(dir, c, RecursionType.BREADTH_FIRST);
202
    }
202
    }
203
 
203
 
204
    public static void walk(File dir, IClosure<File> c, RecursionType type) {
204
    public static void walk(File dir, IClosure<File> c, RecursionType type) {
205
        if (type == RecursionType.BREADTH_FIRST)
205
        if (type == RecursionType.BREADTH_FIRST)
206
            c.executeChecked(dir);
206
            c.executeChecked(dir);
207
        if (dir.isDirectory()) {
207
        if (dir.isDirectory()) {
208
            for (final File child : dir.listFiles()) {
208
            for (final File child : dir.listFiles()) {
209
                walk(child, c, type);
209
                walk(child, c, type);
210
            }
210
            }
211
        }
211
        }
212
        if (type == RecursionType.DEPTH_FIRST)
212
        if (type == RecursionType.DEPTH_FIRST)
213
            c.executeChecked(dir);
213
            c.executeChecked(dir);
214
    }
214
    }
215
 
215
 
216
    public static final List<File> list(File root, final int depth) {
216
    public static final List<File> list(File root, final int depth) {
217
        return list(root, depth, null);
217
        return list(root, depth, null);
218
    }
218
    }
219
 
219
 
220
    /**
220
    /**
221
     * Finds all files at the specified depth below <code>root</code>.
221
     * Finds all files at the specified depth below <code>root</code>.
222
     * 
222
     * 
223
     * @param root the base directory
223
     * @param root the base directory
224
     * @param depth the depth of the returned files.
224
     * @param depth the depth of the returned files.
225
     * @param ff a filter, can be <code>null</code>.
225
     * @param ff a filter, can be <code>null</code>.
226
     * @return a list of files <code>depth</code> levels beneath <code>root</code>.
226
     * @return a list of files <code>depth</code> levels beneath <code>root</code>.
227
     */
227
     */
228
    public static final List<File> list(File root, final int depth, final FileFilter ff) {
228
    public static final List<File> list(File root, final int depth, final FileFilter ff) {
229
        return list(root, depth, depth, ff);
229
        return list(root, depth, depth, ff);
230
    }
230
    }
231
 
231
 
232
    public static final List<File> list(final File root, final int minDepth, final int maxDepth, final FileFilter ff) {
232
    public static final List<File> list(final File root, final int minDepth, final int maxDepth, final FileFilter ff) {
233
        return list(root, minDepth, maxDepth, ff, false);
233
        return list(root, minDepth, maxDepth, ff, false);
234
    }
234
    }
235
 
235
 
236
    public static final List<File> list(final File root, final int minDepth, final int maxDepth, final FileFilter ff, final boolean sort) {
236
    public static final List<File> list(final File root, final int minDepth, final int maxDepth, final FileFilter ff, final boolean sort) {
237
        if (minDepth > maxDepth)
237
        if (minDepth > maxDepth)
238
            throw new IllegalArgumentException(minDepth + " > " + maxDepth);
238
            throw new IllegalArgumentException(minDepth + " > " + maxDepth);
239
        if (maxDepth < 0)
239
        if (maxDepth < 0)
240
            throw new IllegalArgumentException(maxDepth + " < 0");
240
            throw new IllegalArgumentException(maxDepth + " < 0");
241
        if (!root.exists())
241
        if (!root.exists())
242
            return Collections.<File> emptyList();
242
            return Collections.<File> emptyList();
243
 
243
 
244
        final File currentFile = accept(ff, minDepth, maxDepth, root, 0) ? root : null;
244
        final File currentFile = accept(ff, minDepth, maxDepth, root, 0) ? root : null;
245
        if (maxDepth == 0) {
245
        if (maxDepth == 0) {
246
            return currentFile == null ? Collections.<File> emptyList() : Collections.singletonList(currentFile);
246
            return currentFile == null ? Collections.<File> emptyList() : Collections.singletonList(currentFile);
247
        } else {
247
        } else {
248
            final List<File> res = new ArrayList<File>();
248
            final List<File> res = new ArrayList<File>();
249
            final File[] children = root.listFiles();
249
            final File[] children = root.listFiles();
250
            if (children == null)
250
            if (children == null)
251
                throw new IllegalStateException("cannot list " + root);
251
                throw new IllegalStateException("cannot list " + root);
252
            if (sort)
252
            if (sort)
253
                Arrays.sort(children);
253
                Arrays.sort(children);
254
            for (final File child : children) {
254
            for (final File child : children) {
255
                if (maxDepth > 1 && child.isDirectory()) {
255
                if (maxDepth > 1 && child.isDirectory()) {
256
                    res.addAll(list(child, minDepth - 1, maxDepth - 1, ff, sort));
256
                    res.addAll(list(child, minDepth - 1, maxDepth - 1, ff, sort));
257
                } else if (accept(ff, minDepth, maxDepth, child, 1)) {
257
                } else if (accept(ff, minDepth, maxDepth, child, 1)) {
258
                    res.add(child);
258
                    res.add(child);
259
                }
259
                }
260
            }
260
            }
261
            if (currentFile != null)
261
            if (currentFile != null)
262
                res.add(currentFile);
262
                res.add(currentFile);
263
            return res;
263
            return res;
264
        }
264
        }
265
    }
265
    }
266
 
266
 
267
    private static final boolean accept(final FileFilter ff, final int minDepth, final int maxDepth, final File f, final int depth) {
267
    private static final boolean accept(final FileFilter ff, final int minDepth, final int maxDepth, final File f, final int depth) {
268
        return minDepth <= depth && depth <= maxDepth && (ff == null || ff.accept(f));
268
        return minDepth <= depth && depth <= maxDepth && (ff == null || ff.accept(f));
269
    }
269
    }
270
 
270
 
271
    /**
271
    /**
272
     * Returns the relative path from one file to another in the same filesystem tree. Files are not
272
     * Returns the relative path from one file to another in the same filesystem tree. Files are not
273
     * required to exist, see {@link File#getCanonicalPath()}.
273
     * required to exist, see {@link File#getCanonicalPath()}.
274
     * 
274
     * 
275
     * @param fromDir the starting directory, eg /a/b/.
275
     * @param fromDir the starting directory, eg /a/b/.
276
     * @param to the file to get to, eg /a/x/y.txt.
276
     * @param to the file to get to, eg /a/x/y.txt.
277
     * @return the relative path, eg "../x/y.txt".
277
     * @return the relative path, eg "../x/y.txt".
278
     * @throws IOException if an error occurs while canonicalizing the files.
278
     * @throws IOException if an error occurs while canonicalizing the files.
279
     * @throws IllegalArgumentException if fromDir exists and is not directory.
279
     * @throws IllegalArgumentException if fromDir exists and is not directory.
-
 
280
     * @see {@link Path#relativize(Path)}
280
     */
281
     */
281
    public static final String relative(File fromDir, File to) throws IOException {
282
    public static final String relative(File fromDir, File to) throws IOException {
282
        if (fromDir.exists() && !fromDir.isDirectory())
283
        if (fromDir.exists() && !fromDir.isDirectory())
283
            throw new IllegalArgumentException(fromDir + " is not a directory");
284
            throw new IllegalArgumentException(fromDir + " is not a directory");
284
 
285
 
285
        final File fromF = fromDir.getCanonicalFile();
286
        final File fromF = fromDir.getCanonicalFile();
286
        final File toF = to.getCanonicalFile();
287
        final File toF = to.getCanonicalFile();
287
        final List<File> toPath = getAncestors(toF);
288
        final List<File> toPath = getAncestors(toF);
288
        final List<File> fromPath = getAncestors(fromF);
289
        final List<File> fromPath = getAncestors(fromF);
289
 
290
 
290
        // no common ancestor (for example on Windows on 2 different letters)
291
        // no common ancestor (for example on Windows on 2 different letters)
291
        if (!toPath.get(0).equals(fromPath.get(0))) {
292
        if (!toPath.get(0).equals(fromPath.get(0))) {
292
            // already canonical
293
            // already canonical
293
            return toF.getPath();
294
            return toF.getPath();
294
        }
295
        }
295
 
296
 
296
        int commonIndex = Math.min(toPath.size(), fromPath.size()) - 1;
297
        int commonIndex = Math.min(toPath.size(), fromPath.size()) - 1;
297
        boolean found = false;
298
        boolean found = false;
298
        while (commonIndex >= 0 && !found) {
299
        while (commonIndex >= 0 && !found) {
299
            found = fromPath.get(commonIndex).equals(toPath.get(commonIndex));
300
            found = fromPath.get(commonIndex).equals(toPath.get(commonIndex));
300
            if (!found)
301
            if (!found)
301
                commonIndex--;
302
                commonIndex--;
302
        }
303
        }
303
 
304
 
304
        // on remonte jusqu'à l'ancêtre commun
305
        // on remonte jusqu'à l'ancêtre commun
305
        final List<String> complete = new ArrayList<String>(Collections.nCopies(fromPath.size() - 1 - commonIndex, ".."));
306
        final List<String> complete = new ArrayList<String>(Collections.nCopies(fromPath.size() - 1 - commonIndex, ".."));
306
        if (complete.isEmpty())
307
        if (complete.isEmpty())
307
            complete.add(".");
308
            complete.add(".");
308
        // puis on descend vers 'to'
309
        // puis on descend vers 'to'
309
        for (File f : toPath.subList(commonIndex + 1, toPath.size())) {
310
        for (File f : toPath.subList(commonIndex + 1, toPath.size())) {
310
            complete.add(f.getName());
311
            complete.add(f.getName());
311
        }
312
        }
312
 
313
 
313
        return CollectionUtils.join(complete, File.separator);
314
        return CollectionUtils.join(complete, File.separator);
314
    }
315
    }
315
 
316
 
316
    // return each ancestor of f (including itself)
317
    // return each ancestor of f (including itself)
317
    // eg [/, /folder, /folder/dir] for /folder/dir
318
    // eg [/, /folder, /folder/dir] for /folder/dir
318
    public final static List<File> getAncestors(File f) {
319
    public final static List<File> getAncestors(File f) {
319
        final List<File> path = new ArrayList<File>();
320
        final List<File> path = new ArrayList<File>();
320
        File currentF = f;
321
        File currentF = f;
321
        while (currentF != null) {
322
        while (currentF != null) {
322
            path.add(0, currentF);
323
            path.add(0, currentF);
323
            currentF = currentF.getParentFile();
324
            currentF = currentF.getParentFile();
324
        }
325
        }
325
        return path;
326
        return path;
326
    }
327
    }
327
 
328
 
328
    public final static File addSuffix(File f, String suffix) {
329
    public final static File addSuffix(File f, String suffix) {
329
        return new File(f.getParentFile(), f.getName() + suffix);
330
        return new File(f.getParentFile(), f.getName() + suffix);
330
    }
331
    }
331
 
332
 
332
    /**
333
    /**
333
     * Prepend a string to a suffix.
334
     * Prepend a string to a suffix.
334
     * 
335
     * 
335
     * @param f the file, e.g. "sample.xml".
336
     * @param f the file, e.g. "sample.xml".
336
     * @param toInsert the string to insert in the filename, e.g. "-sql".
337
     * @param toInsert the string to insert in the filename, e.g. "-sql".
337
     * @param suffix the suffix of <code>f</code>, e.g. ".xml".
338
     * @param suffix the suffix of <code>f</code>, e.g. ".xml".
338
     * @return a new file with <code>toInsert</code> prepended to <code>suffix</code>, e.g.
339
     * @return a new file with <code>toInsert</code> prepended to <code>suffix</code>, e.g.
339
     *         "sample-sql.xml".
340
     *         "sample-sql.xml".
340
     */
341
     */
341
    public final static File prependSuffix(File f, String toInsert, String suffix) {
342
    public final static File prependSuffix(File f, String toInsert, String suffix) {
342
        return new File(f.getParentFile(), removeSuffix(f.getName(), suffix) + toInsert + suffix);
343
        return new File(f.getParentFile(), removeSuffix(f.getName(), suffix) + toInsert + suffix);
343
    }
344
    }
344
 
345
 
345
    /**
346
    /**
346
     * Prepend a string to a suffix.
347
     * Prepend a string to a suffix.
347
     * 
348
     * 
348
     * @param f the file, e.g. "sample.xml".
349
     * @param f the file, e.g. "sample.xml".
349
     * @param toInsert the string to insert in the filename, e.g. "-sql".
350
     * @param toInsert the string to insert in the filename, e.g. "-sql".
350
     * @param suffix the suffix of <code>f</code>, e.g. ".xml".
351
     * @param suffix the suffix of <code>f</code>, e.g. ".xml".
351
     * @return a new file with <code>toInsert</code> prepended to <code>suffix</code>, e.g.
352
     * @return a new file with <code>toInsert</code> prepended to <code>suffix</code>, e.g.
352
     *         "sample-sql.xml".
353
     *         "sample-sql.xml".
353
     */
354
     */
354
    public final static Path prependSuffix(Path f, String toInsert, String suffix) {
355
    public final static Path prependSuffix(Path f, String toInsert, String suffix) {
355
        return f.resolveSibling(removeSuffix(f.getFileName().toString(), suffix) + toInsert + suffix);
356
        return f.resolveSibling(removeSuffix(f.getFileName().toString(), suffix) + toInsert + suffix);
356
    }
357
    }
357
 
358
 
358
    public final static String removeSuffix(String name, String suffix) {
359
    public final static String removeSuffix(String name, String suffix) {
359
        return name.endsWith(suffix) ? name.substring(0, name.length() - suffix.length()) : name;
360
        return name.endsWith(suffix) ? name.substring(0, name.length() - suffix.length()) : name;
360
    }
361
    }
361
 
362
 
362
    /**
363
    /**
363
     * Rename a file if necessary by finding a free name. The tested names are
364
     * Rename a file if necessary by finding a free name. The tested names are
364
     * <code>name + "_" + i + suffix</code>.
365
     * <code>name + "_" + i + suffix</code>.
365
     * 
366
     * 
366
     * @param parent the directory.
367
     * @param parent the directory.
367
     * @param name the base name of the file.
368
     * @param name the base name of the file.
368
     * @param suffix the suffix of the file, e.g. ".ods".
369
     * @param suffix the suffix of the file, e.g. ".ods".
369
     * @return <code>new File(parent, name + suffix)</code> (always non existing) and the new file,
370
     * @return <code>new File(parent, name + suffix)</code> (always non existing) and the new file,
370
     *         (or <code>null</code> if no file was moved).
371
     *         (or <code>null</code> if no file was moved).
371
     */
372
     */
372
    public final static File[] mvOut(final File parent, final String name, final String suffix) {
373
    public final static File[] mvOut(final File parent, final String name, final String suffix) {
373
        final File fDest = new File(parent, name + suffix);
374
        final File fDest = new File(parent, name + suffix);
374
        final File renamed;
375
        final File renamed;
375
        if (fDest.exists()) {
376
        if (fDest.exists()) {
376
            int i = 0;
377
            int i = 0;
377
            File free = fDest;
378
            File free = fDest;
378
            while (free.exists()) {
379
            while (free.exists()) {
379
                free = new File(parent, name + "_" + i + suffix);
380
                free = new File(parent, name + "_" + i + suffix);
380
                i++;
381
                i++;
381
            }
382
            }
382
            assert !fDest.equals(free);
383
            assert !fDest.equals(free);
383
            if (!fDest.renameTo(free))
384
            if (!fDest.renameTo(free))
384
                throw new IllegalStateException("Couldn't rename " + fDest + " to " + free);
385
                throw new IllegalStateException("Couldn't rename " + fDest + " to " + free);
385
            renamed = free;
386
            renamed = free;
386
        } else {
387
        } else {
387
            renamed = null;
388
            renamed = null;
388
        }
389
        }
389
        assert !fDest.exists();
390
        assert !fDest.exists();
390
        return new File[] { fDest, renamed };
391
        return new File[] { fDest, renamed };
391
    }
392
    }
392
 
393
 
393
    // ** shell
394
    // ** shell
394
 
395
 
395
    /**
396
    /**
396
     * Behave like the 'mv' unix utility, ie handle cross filesystems mv and <code>dest</code> being
397
     * Behave like the 'mv' unix utility, ie handle cross filesystems mv and <code>dest</code> being
397
     * a directory.
398
     * a directory.
398
     * 
399
     * 
399
     * @param f the source file.
400
     * @param f the source file.
400
     * @param dest the destination file or directory.
401
     * @param dest the destination file or directory.
401
     * @return the error or <code>null</code> if there was none.
402
     * @return the error or <code>null</code> if there was none.
402
     */
403
     */
403
    public static String mv(File f, File dest) {
404
    public static String mv(File f, File dest) {
404
        final File canonF;
405
        final File canonF;
405
        File canonDest;
406
        File canonDest;
406
        try {
407
        try {
407
            canonF = f.getCanonicalFile();
408
            canonF = f.getCanonicalFile();
408
            canonDest = dest.getCanonicalFile();
409
            canonDest = dest.getCanonicalFile();
409
        } catch (IOException e) {
410
        } catch (IOException e) {
410
            return ExceptionUtils.getStackTrace(e);
411
            return ExceptionUtils.getStackTrace(e);
411
        }
412
        }
412
        if (canonF.equals(canonDest))
413
        if (canonF.equals(canonDest))
413
            // nothing to do
414
            // nothing to do
414
            return null;
415
            return null;
415
        if (canonDest.isDirectory())
416
        if (canonDest.isDirectory())
416
            canonDest = new File(canonDest, canonF.getName());
417
            canonDest = new File(canonDest, canonF.getName());
417
 
418
 
418
        final File destF;
419
        final File destF;
419
        if (canonDest.exists())
420
        if (canonDest.exists())
420
            return canonDest + " exists";
421
            return canonDest + " exists";
421
        else if (!canonDest.getParentFile().exists())
422
        else if (!canonDest.getParentFile().exists())
422
            return "parent of " + canonDest + " does not exist";
423
            return "parent of " + canonDest + " does not exist";
423
        else
424
        else
424
            destF = canonDest;
425
            destF = canonDest;
425
        if (!canonF.renameTo(destF)) {
426
        if (!canonF.renameTo(destF)) {
426
            try {
427
            try {
427
                copyDirectory(canonF, destF);
428
                copyDirectory(canonF, destF);
428
                if (destF.exists())
429
                if (destF.exists())
429
                    rm_R(canonF);
430
                    rm_R(canonF);
430
            } catch (IOException e) {
431
            } catch (IOException e) {
431
                return ExceptionUtils.getStackTrace(e);
432
                return ExceptionUtils.getStackTrace(e);
432
            }
433
            }
433
        }
434
        }
434
        return null;
435
        return null;
435
    }
436
    }
436
 
437
 
437
    // transferTo() can be limited by a number of factors, like the number of bits of the system
438
    // transferTo() can be limited by a number of factors, like the number of bits of the system
438
    // if mmap is used (e.g. on Linux) or by an arbitrary magic number on Windows : 64Mb - 32Kb
439
    // if mmap is used (e.g. on Linux) or by an arbitrary magic number on Windows : 64Mb - 32Kb
439
    private static final int CHANNEL_MAX_COUNT = Math.min(64 * 1024 * 1024 - 32 * 1024, Integer.MAX_VALUE);
440
    private static final int CHANNEL_MAX_COUNT = Math.min(64 * 1024 * 1024 - 32 * 1024, Integer.MAX_VALUE);
440
 
441
 
441
    public static void copyFile(File in, File out) throws IOException {
442
    public static void copyFile(File in, File out) throws IOException {
442
        copyFile(in, out, CHANNEL_MAX_COUNT);
443
        copyFile(in, out, CHANNEL_MAX_COUNT);
443
    }
444
    }
444
 
445
 
445
    /**
446
    /**
446
     * Copy a file. It is generally not advised to use 0 for <code>maxCount</code> since various
447
     * Copy a file. It is generally not advised to use 0 for <code>maxCount</code> since various
447
     * implementations have size limitations, see {@link #copyFile(File, File)}.
448
     * implementations have size limitations, see {@link #copyFile(File, File)}.
448
     * 
449
     * 
449
     * @param in the source file.
450
     * @param in the source file.
450
     * @param out the destination file.
451
     * @param out the destination file.
451
     * @param maxCount the number of bytes to copy at a time, 0 meaning size of <code>in</code>.
452
     * @param maxCount the number of bytes to copy at a time, 0 meaning size of <code>in</code>.
452
     * @throws IOException if an error occurs.
453
     * @throws IOException if an error occurs.
453
     */
454
     */
454
    public static void copyFile(File in, File out, long maxCount) throws IOException {
455
    public static void copyFile(File in, File out, long maxCount) throws IOException {
455
        try (final FileInputStream sourceIn = new FileInputStream(in); final FileOutputStream sourceOut = new FileOutputStream(out);) {
456
        try (final FileInputStream sourceIn = new FileInputStream(in); final FileOutputStream sourceOut = new FileOutputStream(out);) {
456
            final FileChannel sourceChannel = sourceIn.getChannel();
457
            final FileChannel sourceChannel = sourceIn.getChannel();
457
            final long size = sourceChannel.size();
458
            final long size = sourceChannel.size();
458
            if (maxCount == 0)
459
            if (maxCount == 0)
459
                maxCount = size;
460
                maxCount = size;
460
            final FileChannel destinationChannel = sourceOut.getChannel();
461
            final FileChannel destinationChannel = sourceOut.getChannel();
461
            long position = 0;
462
            long position = 0;
462
            while (position < size) {
463
            while (position < size) {
463
                position += sourceChannel.transferTo(position, maxCount, destinationChannel);
464
                position += sourceChannel.transferTo(position, maxCount, destinationChannel);
464
            }
465
            }
465
        }
466
        }
466
    }
467
    }
467
 
468
 
468
    public static void copyFile(File in, File out, final boolean useTime) throws IOException {
469
    public static void copyFile(File in, File out, final boolean useTime) throws IOException {
469
        if (!useTime || in.lastModified() != out.lastModified()) {
470
        if (!useTime || in.lastModified() != out.lastModified()) {
470
            copyFile(in, out);
471
            copyFile(in, out);
471
            if (useTime)
472
            if (useTime)
472
                out.setLastModified(in.lastModified());
473
                out.setLastModified(in.lastModified());
473
        }
474
        }
474
    }
475
    }
475
 
476
 
476
    public static void copyDirectory(File in, File out) throws IOException {
477
    public static void copyDirectory(File in, File out) throws IOException {
477
        copyDirectory(in, out, Collections.<String> emptySet());
478
        copyDirectory(in, out, Collections.<String> emptySet());
478
    }
479
    }
479
 
480
 
480
    public static final Set<String> VersionControl = CollectionUtils.createSet(".svn", "CVS");
481
    public static final Set<String> VersionControl = CollectionUtils.createSet(".svn", "CVS");
481
 
482
 
482
    public static void copyDirectory(File in, File out, final Set<String> toIgnore) throws IOException {
483
    public static void copyDirectory(File in, File out, final Set<String> toIgnore) throws IOException {
483
        copyDirectory(in, out, toIgnore, false);
484
        copyDirectory(in, out, toIgnore, false);
484
    }
485
    }
485
 
486
 
486
    public static void copyDirectory(File in, File out, final Set<String> toIgnore, final boolean useTime) throws IOException {
487
    public static void copyDirectory(File in, File out, final Set<String> toIgnore, final boolean useTime) throws IOException {
487
        if (toIgnore.contains(in.getName()))
488
        if (toIgnore.contains(in.getName()))
488
            return;
489
            return;
489
 
490
 
490
        if (in.isDirectory()) {
491
        if (in.isDirectory()) {
491
            if (!out.exists()) {
492
            if (!out.exists()) {
492
                out.mkdir();
493
                out.mkdir();
493
            }
494
            }
494
 
495
 
495
            String[] children = in.list();
496
            String[] children = in.list();
496
            for (int i = 0; i < children.length; i++) {
497
            for (int i = 0; i < children.length; i++) {
497
                copyDirectory(new File(in, children[i]), new File(out, children[i]), toIgnore, useTime);
498
                copyDirectory(new File(in, children[i]), new File(out, children[i]), toIgnore, useTime);
498
            }
499
            }
499
        } else {
500
        } else {
500
            if (!in.getName().equals("Thumbs.db")) {
501
            if (!in.getName().equals("Thumbs.db")) {
501
                copyFile(in, out, useTime);
502
                copyFile(in, out, useTime);
502
            }
503
            }
503
        }
504
        }
504
    }
505
    }
505
 
506
 
506
    public static void copyDirectory(Path in, Path out) throws IOException {
507
    public static void copyDirectory(Path in, Path out) throws IOException {
507
        copyDirectory(in, out, false);
508
        copyDirectory(in, out, false);
508
    }
509
    }
509
 
510
 
510
    public static void copyDirectory(Path in, Path out, final boolean hardLink, final CopyOption... copyOptions) throws IOException {
511
    public static void copyDirectory(Path in, Path out, final boolean hardLink, final CopyOption... copyOptions) throws IOException {
511
        copyDirectory(in, out, Collections.<String> emptySet(), hardLink, false, Arrays.asList(copyOptions));
512
        copyDirectory(in, out, Collections.<String> emptySet(), hardLink, false, Arrays.asList(copyOptions));
512
    }
513
    }
513
 
514
 
514
    public static void copyDirectory(Path in, Path out, final Set<String> toIgnore, final boolean hardLink, final boolean followLinks, final List<CopyOption> copyOptions) throws IOException {
515
    public static void copyDirectory(Path in, Path out, final Set<String> toIgnore, final boolean hardLink, final boolean followLinks, final List<CopyOption> copyOptions) throws IOException {
515
        final LinkOption[] isDirOptions = followLinks ? new LinkOption[0] : new LinkOption[] { LinkOption.NOFOLLOW_LINKS };
516
        final LinkOption[] isDirOptions = followLinks ? new LinkOption[0] : new LinkOption[] { LinkOption.NOFOLLOW_LINKS };
516
        final Set<CopyOption> copyOptionsP = new HashSet<>(copyOptions);
517
        final Set<CopyOption> copyOptionsP = new HashSet<>(copyOptions);
517
        if (followLinks) {
518
        if (followLinks) {
518
            copyOptionsP.remove(LinkOption.NOFOLLOW_LINKS);
519
            copyOptionsP.remove(LinkOption.NOFOLLOW_LINKS);
519
        } else {
520
        } else {
520
            copyOptionsP.add(LinkOption.NOFOLLOW_LINKS);
521
            copyOptionsP.add(LinkOption.NOFOLLOW_LINKS);
521
        }
522
        }
522
        copyDirectory(in, out, toIgnore, isDirOptions, copyOptionsP.toArray(new CopyOption[copyOptionsP.size()]), hardLink);
523
        copyDirectory(in, out, toIgnore, isDirOptions, copyOptionsP.toArray(new CopyOption[copyOptionsP.size()]), hardLink);
523
    }
524
    }
524
 
525
 
525
    public static void copyDirectory(Path in, Path out, final Set<String> toIgnore, final LinkOption[] isDirOptions, final CopyOption[] copyOptions, final boolean hardLink) throws IOException {
526
    public static void copyDirectory(Path in, Path out, final Set<String> toIgnore, final LinkOption[] isDirOptions, final CopyOption[] copyOptions, final boolean hardLink) throws IOException {
526
        if (toIgnore.contains(in.getFileName().toString()))
527
        if (toIgnore.contains(in.getFileName().toString()))
527
            return;
528
            return;
528
 
529
 
529
        if (Files.isDirectory(in, isDirOptions)) {
530
        if (Files.isDirectory(in, isDirOptions)) {
530
            Files.copy(in, out, copyOptions);
531
            Files.copy(in, out, copyOptions);
531
 
532
 
532
            try (final DirectoryStream<Path> dirStream = Files.newDirectoryStream(in)) {
533
            try (final DirectoryStream<Path> dirStream = Files.newDirectoryStream(in)) {
533
                for (final Path child : dirStream) {
534
                for (final Path child : dirStream) {
534
                    copyDirectory(child, out.resolve(child.getFileName()), toIgnore, isDirOptions, copyOptions, hardLink);
535
                    copyDirectory(child, out.resolve(child.getFileName()), toIgnore, isDirOptions, copyOptions, hardLink);
535
                }
536
                }
536
            }
537
            }
537
            // fix up modification time of directory when done
538
            // fix up modification time of directory when done
538
            // (other attributes have already been copied at creation time)
539
            // (other attributes have already been copied at creation time)
539
            if (Arrays.asList(copyOptions).contains(StandardCopyOption.COPY_ATTRIBUTES))
540
            if (Arrays.asList(copyOptions).contains(StandardCopyOption.COPY_ATTRIBUTES))
540
                Files.setLastModifiedTime(out, Files.getLastModifiedTime(in));
541
                Files.setLastModifiedTime(out, Files.getLastModifiedTime(in));
541
        } else {
542
        } else {
542
            if (!in.getFileName().toString().equals("Thumbs.db")) {
543
            if (!in.getFileName().toString().equals("Thumbs.db")) {
543
                if (hardLink)
544
                if (hardLink)
544
                    Files.createLink(out, in);
545
                    Files.createLink(out, in);
545
                else
546
                else
546
                    Files.copy(in, out, copyOptions);
547
                    Files.copy(in, out, copyOptions);
547
            }
548
            }
548
        }
549
        }
549
    }
550
    }
550
 
551
 
551
    /**
552
    /**
552
     * Delete recursively the passed directory. If a deletion fails, the method stops attempting to
553
     * Delete recursively the passed directory. If a deletion fails, the method stops attempting to
553
     * delete and returns false.
554
     * delete and returns false.
554
     * 
555
     * 
555
     * @param dir the dir to be deleted.
556
     * @param dir the dir to be deleted.
556
     * @return <code>true</code> if all deletions were successful.
557
     * @return <code>true</code> if all deletions were successful.
557
     * @deprecated callers often forget to check return value, see also {@link #rm_R(Path)}
558
     * @deprecated callers often forget to check return value, see also {@link #rm_R(Path)}
558
     */
559
     */
559
    public static boolean rmR(File dir) {
560
    public static boolean rmR(File dir) {
560
        if (dir.isDirectory()) {
561
        if (dir.isDirectory()) {
561
            File[] children = dir.listFiles();
562
            File[] children = dir.listFiles();
562
            for (int i = 0; i < children.length; i++) {
563
            for (int i = 0; i < children.length; i++) {
563
                boolean success = rmR(children[i]);
564
                boolean success = rmR(children[i]);
564
 
565
 
565
                if (!success) {
566
                if (!success) {
566
 
567
 
567
                    return false;
568
                    return false;
568
                }
569
                }
569
            }
570
            }
570
        }
571
        }
571
 
572
 
572
        // The directory is now empty so delete it
573
        // The directory is now empty so delete it
573
        return dir.delete();
574
        return dir.delete();
574
    }
575
    }
575
 
576
 
576
    public static void rm_R(File dir) throws IOException {
577
    public static void rm_R(File dir) throws IOException {
577
        rm_R(dir.toPath());
578
        rm_R(dir.toPath());
578
    }
579
    }
579
 
580
 
580
    public static void rm_R(Path dir) throws IOException {
581
    public static void rm_R(Path dir) throws IOException {
581
        rm_R(dir, false);
582
        rm_R(dir, false);
582
    }
583
    }
583
 
584
 
584
    /**
585
    /**
585
     * Delete recursively the passed file.
586
     * Delete recursively the passed file.
586
     * 
587
     * 
587
     * @param dir the file to delete.
588
     * @param dir the file to delete.
588
     * @param mustExist what to do if <code>dir</code> is missing : <code>false</code> if it is not
589
     * @param mustExist what to do if <code>dir</code> is missing : <code>false</code> if it is not
589
     *        an error, <code>true</code> to fail.
590
     *        an error, <code>true</code> to fail.
590
     * @throws IOException if a deletion fails.
591
     * @throws IOException if a deletion fails.
591
     */
592
     */
592
    public static void rm_R(final Path dir, final boolean mustExist) throws IOException {
593
    public static void rm_R(final Path dir, final boolean mustExist) throws IOException {
593
        if (!mustExist && !Files.exists(dir))
594
        if (!mustExist && !Files.exists(dir))
594
            return;
595
            return;
595
        Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
596
        Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
596
            @Override
597
            @Override
597
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
598
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
598
                Files.delete(file);
599
                Files.delete(file);
599
                return FileVisitResult.CONTINUE;
600
                return FileVisitResult.CONTINUE;
600
            }
601
            }
601
 
602
 
602
            @Override
603
            @Override
603
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
604
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
604
                // The directory is now empty so delete it
605
                // The directory is now empty so delete it
605
                Files.delete(dir);
606
                Files.delete(dir);
606
                return FileVisitResult.CONTINUE;
607
                return FileVisitResult.CONTINUE;
607
            }
608
            }
608
        });
609
        });
609
    }
610
    }
610
 
611
 
611
    public static void rm(File f) throws IOException {
612
    public static void rm(File f) throws IOException {
612
        if (f.exists() && !f.delete())
613
        if (f.exists() && !f.delete())
613
            throw new IOException("cannot delete " + f);
614
            throw new IOException("cannot delete " + f);
614
    }
615
    }
615
 
616
 
616
    public static final File mkdir_p(File dir) throws IOException {
617
    public static final File mkdir_p(File dir) throws IOException {
617
        if (!dir.exists()) {
618
        if (!dir.exists()) {
618
            if (!dir.mkdirs()) {
619
            if (!dir.mkdirs()) {
619
                throw new IOException("cannot create directory " + dir);
620
                throw new IOException("cannot create directory " + dir);
620
            }
621
            }
621
        }
622
        }
622
        return dir;
623
        return dir;
623
    }
624
    }
624
 
625
 
625
    /**
626
    /**
626
     * Create all ancestors of <code>f</code>.
627
     * Create all ancestors of <code>f</code>.
627
     * 
628
     * 
628
     * @param f any file whose ancestors should be created.
629
     * @param f any file whose ancestors should be created.
629
     * @return <code>f</code>.
630
     * @return <code>f</code>.
630
     * @throws IOException if ancestors cannot be created.
631
     * @throws IOException if ancestors cannot be created.
631
     */
632
     */
632
    public static final File mkParentDirs(File f) throws IOException {
633
    public static final File mkParentDirs(File f) throws IOException {
633
        final File parentFile = f.getParentFile();
634
        final File parentFile = f.getParentFile();
634
        if (parentFile != null)
635
        if (parentFile != null)
635
            mkdir_p(parentFile);
636
            mkdir_p(parentFile);
636
        return f;
637
        return f;
637
    }
638
    }
638
 
639
 
639
    // **io
640
    // **io
640
 
641
 
641
    /**
642
    /**
642
     * Read a file line by line with the default encoding and returns the concatenation of these.
643
     * Read a file line by line with the default encoding and returns the concatenation of these.
643
     * 
644
     * 
644
     * @param f the file to read.
645
     * @param f the file to read.
645
     * @return the content of f.
646
     * @return the content of f.
646
     * @throws IOException if a pb occur while reading.
647
     * @throws IOException if a pb occur while reading.
647
     */
648
     */
648
    public static final String read(File f) throws IOException {
649
    public static final String read(File f) throws IOException {
649
        return read(new InputStreamReader(new FileInputStream(f)));
650
        return read(new InputStreamReader(new FileInputStream(f)));
650
    }
651
    }
651
 
652
 
652
    /**
653
    /**
653
     * Read a file line by line and returns the concatenation of these.
654
     * Read a file line by line and returns the concatenation of these.
654
     * 
655
     * 
655
     * @param f the file to read.
656
     * @param f the file to read.
656
     * @param charset the encoding of <code>f</code>.
657
     * @param charset the encoding of <code>f</code>.
657
     * @return the content of f.
658
     * @return the content of f.
658
     * @throws IOException if a pb occur while reading.
659
     * @throws IOException if a pb occur while reading.
659
     */
660
     */
660
    public static final String read(File f, String charset) throws IOException {
661
    public static final String read(File f, String charset) throws IOException {
661
        return read(new InputStreamReader(new FileInputStream(f), charset));
662
        return read(new InputStreamReader(new FileInputStream(f), charset));
662
    }
663
    }
663
 
664
 
664
    public static final String readUTF8(File f) throws IOException {
665
    public static final String readUTF8(File f) throws IOException {
665
        return readUTF8(new FileInputStream(f));
666
        return readUTF8(new FileInputStream(f));
666
    }
667
    }
667
 
668
 
668
    public static final String readUTF8(Path p) throws IOException {
669
    public static final String readUTF8(Path p) throws IOException {
669
        return new String(Files.readAllBytes(p), StandardCharsets.UTF_8);
670
        return new String(Files.readAllBytes(p), StandardCharsets.UTF_8);
670
    }
671
    }
671
 
672
 
672
    public static final String readUTF8(InputStream ins) throws IOException {
673
    public static final String readUTF8(InputStream ins) throws IOException {
673
        return read(ins, StringUtils.UTF8);
674
        return read(ins, StringUtils.UTF8);
674
    }
675
    }
675
 
676
 
676
    public static final String read(File f, Charset charset) throws IOException {
677
    public static final String read(File f, Charset charset) throws IOException {
677
        return read(new FileInputStream(f), charset);
678
        return read(new FileInputStream(f), charset);
678
    }
679
    }
679
 
680
 
680
    public static final String read(InputStream ins, Charset charset) throws IOException {
681
    public static final String read(InputStream ins, Charset charset) throws IOException {
681
        final Reader reader;
682
        final Reader reader;
682
        if (charset == null)
683
        if (charset == null)
683
            reader = new InputStreamReader(ins);
684
            reader = new InputStreamReader(ins);
684
        else
685
        else
685
            reader = new InputStreamReader(ins, charset);
686
            reader = new InputStreamReader(ins, charset);
686
        return read(reader);
687
        return read(reader);
687
    }
688
    }
688
 
689
 
689
    public static final String read(final Reader reader) throws IOException {
690
    public static final String read(final Reader reader) throws IOException {
690
        return read(reader, 8192);
691
        return read(reader, 8192);
691
    }
692
    }
692
 
693
 
693
    public static final String read(final Reader reader, final int bufferSize) throws IOException {
694
    public static final String read(final Reader reader, final int bufferSize) throws IOException {
694
        final StringBuilder sb = new StringBuilder();
695
        final StringBuilder sb = new StringBuilder();
695
        final char[] buffer = new char[bufferSize];
696
        final char[] buffer = new char[bufferSize];
696
        final BufferedReader in = new BufferedReader(reader);
697
        final BufferedReader in = new BufferedReader(reader);
697
        try {
698
        try {
698
            while (true) {
699
            while (true) {
699
                final int count = in.read(buffer);
700
                final int count = in.read(buffer);
700
                if (count == -1)
701
                if (count == -1)
701
                    break;
702
                    break;
702
                sb.append(buffer, 0, count);
703
                sb.append(buffer, 0, count);
703
            }
704
            }
704
        } finally {
705
        } finally {
705
            in.close();
706
            in.close();
706
        }
707
        }
707
        return sb.toString();
708
        return sb.toString();
708
    }
709
    }
709
 
710
 
710
    /**
711
    /**
711
     * Read the whole content of a file.
712
     * Read the whole content of a file.
712
     * 
713
     * 
713
     * @param f the file to read.
714
     * @param f the file to read.
714
     * @return its content.
715
     * @return its content.
715
     * @throws IOException if a pb occur while reading.
716
     * @throws IOException if a pb occur while reading.
716
     * @see Files#readAllBytes(java.nio.file.Path)
717
     * @see Files#readAllBytes(java.nio.file.Path)
717
     */
718
     */
718
    public static final byte[] readBytes(File f) throws IOException {
719
    public static final byte[] readBytes(File f) throws IOException {
719
        // works for /proc files which report 0 size
720
        // works for /proc files which report 0 size
720
        return Files.readAllBytes(f.toPath());
721
        return Files.readAllBytes(f.toPath());
721
    }
722
    }
722
 
723
 
723
    /**
724
    /**
724
     * Write the passed string with default charset to the passed file, truncating it.
725
     * Write the passed string with default charset to the passed file, truncating it.
725
     * 
726
     * 
726
     * @param s the string.
727
     * @param s the string.
727
     * @param f the file.
728
     * @param f the file.
728
     * @throws IOException if an error occurs.
729
     * @throws IOException if an error occurs.
729
     * @deprecated use {@link #writeUTF8(Path, String, OpenOption...)} or
730
     * @deprecated use {@link #writeUTF8(Path, String, OpenOption...)} or
730
     *             {@link #write2Step(String, Path)}
731
     *             {@link #write2Step(String, Path)}
731
     */
732
     */
732
    public static void write(String s, File f) throws IOException {
733
    public static void write(String s, File f) throws IOException {
733
        write2Step(s, f, Charset.defaultCharset(), false);
734
        write2Step(s, f, Charset.defaultCharset(), false);
734
    }
735
    }
735
 
736
 
736
    public static void writeUTF8(String s, File f) throws IOException {
737
    public static void writeUTF8(String s, File f) throws IOException {
737
        writeUTF8(f.toPath(), s);
738
        writeUTF8(f.toPath(), s);
738
    }
739
    }
739
 
740
 
740
    public static void writeUTF8(Path path, String s, OpenOption... options) throws IOException {
741
    public static void writeUTF8(Path path, String s, OpenOption... options) throws IOException {
741
        Files.write(path, s.getBytes(StandardCharsets.UTF_8), options);
742
        Files.write(path, s.getBytes(StandardCharsets.UTF_8), options);
742
    }
743
    }
743
 
744
 
744
    public static void write2Step(String s, File f, Charset charset, boolean append) throws IOException {
745
    public static void write2Step(String s, File f, Charset charset, boolean append) throws IOException {
745
        write2Step(s, f.toPath(), charset, append);
746
        write2Step(s, f.toPath(), charset, append);
746
    }
747
    }
747
 
748
 
748
    public static void write2Step(final String s, final Path f) throws IOException {
749
    public static void write2Step(final String s, final Path f) throws IOException {
749
        // UTF_8 default like Files.newBufferedReader()
750
        // UTF_8 default like Files.newBufferedReader()
750
        write2Step(s, f, StandardCharsets.UTF_8);
751
        write2Step(s, f, StandardCharsets.UTF_8);
751
    }
752
    }
752
 
753
 
753
    public static void write2Step(final String s, final Path f, final Charset charset) throws IOException {
754
    public static void write2Step(final String s, final Path f, final Charset charset) throws IOException {
754
        write2Step(s, f, charset, false);
755
        write2Step(s, f, charset, false);
755
    }
756
    }
756
 
757
 
757
    public static void write2Step(final String s, final Path f, final Charset charset, final boolean append) throws IOException {
758
    public static void write2Step(final String s, final Path f, final Charset charset, final boolean append) throws IOException {
758
        // create temporary file in the same directory so we can move it
759
        // create temporary file in the same directory so we can move it
759
        final Path tmpFile = Files.createTempFile(f.toAbsolutePath().getParent(), null, null);
760
        final Path tmpFile = Files.createTempFile(f.toAbsolutePath().getParent(), null, null);
760
        try {
761
        try {
761
            // 1. write elsewhere
762
            // 1. write elsewhere
762
            final byte[] bs = charset == null ? s.getBytes() : s.getBytes(charset);
763
            final byte[] bs = charset == null ? s.getBytes() : s.getBytes(charset);
763
            if (append) {
764
            if (append) {
764
                // REPLACE_EXISTING since tmpFile exists
765
                // REPLACE_EXISTING since tmpFile exists
765
                Files.copy(f, tmpFile, StandardCopyOption.REPLACE_EXISTING);
766
                Files.copy(f, tmpFile, StandardCopyOption.REPLACE_EXISTING);
766
                Files.write(tmpFile, bs, StandardOpenOption.APPEND);
767
                Files.write(tmpFile, bs, StandardOpenOption.APPEND);
767
            } else {
768
            } else {
768
                Files.write(tmpFile, bs);
769
                Files.write(tmpFile, bs);
769
            }
770
            }
770
            // 2. move into place (cannot use ATOMIC_MOVE because f might exists ; and we would need
771
            // 2. move into place (cannot use ATOMIC_MOVE because f might exists ; and we would need
771
            // to handle AtomicMoveNotSupportedException)
772
            // to handle AtomicMoveNotSupportedException)
772
            Files.move(tmpFile, f, StandardCopyOption.REPLACE_EXISTING);
773
            Files.move(tmpFile, f, StandardCopyOption.REPLACE_EXISTING);
773
            // don't try to delete if move() successful
774
            // don't try to delete if move() successful
774
        } catch (RuntimeException | Error | IOException e) {
775
        } catch (RuntimeException | Error | IOException e) {
775
            try {
776
            try {
776
                Files.deleteIfExists(tmpFile);
777
                Files.deleteIfExists(tmpFile);
777
            } catch (Exception e1) {
778
            } catch (Exception e1) {
778
                e.addSuppressed(e1);
779
                e.addSuppressed(e1);
779
            }
780
            }
780
            throw e;
781
            throw e;
781
        }
782
        }
782
    }
783
    }
783
 
784
 
784
    /**
785
    /**
785
     * Create a writer for the passed file, and write the XML declaration.
786
     * Create a writer for the passed file, and write the XML declaration.
786
     * 
787
     * 
787
     * @param f a file
788
     * @param f a file
788
     * @return a writer with the same encoding as the XML.
789
     * @return a writer with the same encoding as the XML.
789
     * @throws IOException if an error occurs.
790
     * @throws IOException if an error occurs.
790
     * @see StreamUtils#createXMLWriter(java.io.OutputStream)
791
     * @see StreamUtils#createXMLWriter(java.io.OutputStream)
791
     */
792
     */
792
    public static BufferedWriter createXMLWriter(final File f) throws IOException {
793
    public static BufferedWriter createXMLWriter(final File f) throws IOException {
793
        FileUtils.mkParentDirs(f);
794
        FileUtils.mkParentDirs(f);
794
        final FileOutputStream outs = new FileOutputStream(f);
795
        final FileOutputStream outs = new FileOutputStream(f);
795
        try {
796
        try {
796
            return StreamUtils.createXMLWriter(outs);
797
            return StreamUtils.createXMLWriter(outs);
797
        } catch (RuntimeException e) {
798
        } catch (RuntimeException e) {
798
            outs.close();
799
            outs.close();
799
            throw e;
800
            throw e;
800
        } catch (IOException e) {
801
        } catch (IOException e) {
801
            outs.close();
802
            outs.close();
802
            throw e;
803
            throw e;
803
        }
804
        }
804
    }
805
    }
805
 
806
 
806
    /**
807
    /**
807
     * Create an UTF-8 buffered writer.
808
     * Create an UTF-8 buffered writer.
808
     * 
809
     * 
809
     * @param f the file to write to.
810
     * @param f the file to write to.
810
     * @return a buffered writer.
811
     * @return a buffered writer.
811
     * @throws FileNotFoundException if the file cannot be opened.
812
     * @throws FileNotFoundException if the file cannot be opened.
812
     */
813
     */
813
    public static BufferedWriter createWriter(final File f) throws FileNotFoundException {
814
    public static BufferedWriter createWriter(final File f) throws FileNotFoundException {
814
        return createWriter(f, StringUtils.UTF8);
815
        return createWriter(f, StringUtils.UTF8);
815
    }
816
    }
816
 
817
 
817
    public static BufferedWriter createWriter(final File f, final Charset cs) throws FileNotFoundException {
818
    public static BufferedWriter createWriter(final File f, final Charset cs) throws FileNotFoundException {
818
        final FileOutputStream outs = new FileOutputStream(f);
819
        final FileOutputStream outs = new FileOutputStream(f);
819
        try {
820
        try {
820
            return new BufferedWriter(new OutputStreamWriter(outs, cs));
821
            return new BufferedWriter(new OutputStreamWriter(outs, cs));
821
        } catch (RuntimeException e) {
822
        } catch (RuntimeException e) {
822
            try {
823
            try {
823
                outs.close();
824
                outs.close();
824
            } catch (IOException e1) {
825
            } catch (IOException e1) {
825
                e1.printStackTrace();
826
                e1.printStackTrace();
826
            }
827
            }
827
            throw e;
828
            throw e;
828
        }
829
        }
829
    }
830
    }
830
 
831
 
831
    /**
832
    /**
832
     * Execute the passed transformer with the lock on the passed file.
833
     * Execute the passed transformer with the lock on the passed file.
833
     * 
834
     * 
834
     * @param <T> return type.
835
     * @param <T> return type.
835
     * @param f the file to lock.
836
     * @param f the file to lock.
836
     * @param transf what to do on the file.
837
     * @param transf what to do on the file.
837
     * @return what <code>transf</code> returns.
838
     * @return what <code>transf</code> returns.
838
     * @throws IOException if an error occurs while locking the file.
839
     * @throws IOException if an error occurs while locking the file.
839
     * @throws X if an error occurs while using the file.
840
     * @throws X if an error occurs while using the file.
840
     */
841
     */
841
    public static final <T, X extends Exception> T doWithLock(final File f, ExnTransformer<RandomAccessFile, T, X> transf) throws IOException, X {
842
    public static final <T, X extends Exception> T doWithLock(final File f, ExnTransformer<RandomAccessFile, T, X> transf) throws IOException, X {
842
        mkParentDirs(f);
843
        mkParentDirs(f);
843
        // don't use FileOutputStream : it truncates the file on creation
844
        // don't use FileOutputStream : it truncates the file on creation
844
        // we need write to obtain lock
845
        // we need write to obtain lock
845
        try (final RandomAccessFile out = new RandomAccessFile(f, "rw")) {
846
        try (final RandomAccessFile out = new RandomAccessFile(f, "rw")) {
846
            out.getChannel().lock();
847
            out.getChannel().lock();
847
            return transf.transformChecked(out);
848
            return transf.transformChecked(out);
848
        }
849
        }
849
        // the lock is released on close()
850
        // the lock is released on close()
850
    }
851
    }
851
 
852
 
852
    private static final Map<URL, File> files = new HashMap<URL, File>();
853
    private static final Map<URL, File> files = new HashMap<URL, File>();
853
 
854
 
854
    private static final File getShortCutFile() throws IOException {
855
    private static final File getShortCutFile() throws IOException {
855
        return getFile(FileUtils.class.getResource("shortcut.vbs"));
856
        return getFile(FileUtils.class.getResource("shortcut.vbs"));
856
    }
857
    }
857
 
858
 
858
    // windows cannot execute a string, it demands a file
859
    // windows cannot execute a string, it demands a file
859
    public static final File getFile(final URL url) throws IOException {
860
    public static final File getFile(final URL url) throws IOException {
860
        // avoid unnecessary IO if already a file
861
        // avoid unnecessary IO if already a file
861
        File urlFile = null;
862
        File urlFile = null;
862
        // inexpensive comparison before trying to convert to URI and call the File constructor
863
        // inexpensive comparison before trying to convert to URI and call the File constructor
863
        if ("file".equalsIgnoreCase(url.getProtocol())) {
864
        if ("file".equalsIgnoreCase(url.getProtocol())) {
864
            try {
865
            try {
865
                urlFile = new File(url.toURI());
866
                urlFile = new File(url.toURI());
866
            } catch (Exception e) {
867
            } catch (Exception e) {
867
                Log.get().log(Level.FINER, "couldn't convert to file " + url, e);
868
                Log.get().log(Level.FINER, "couldn't convert to file " + url, e);
868
            }
869
            }
869
        }
870
        }
870
        if (urlFile != null)
871
        if (urlFile != null)
871
            return urlFile;
872
            return urlFile;
872
 
873
 
873
        final File shortcutFile;
874
        final File shortcutFile;
874
        final File currentFile = files.get(url);
875
        final File currentFile = files.get(url);
875
        if (currentFile == null || !currentFile.exists()) {
876
        if (currentFile == null || !currentFile.exists()) {
876
            shortcutFile = File.createTempFile("windowsIsLame", ".vbs");
877
            shortcutFile = File.createTempFile("windowsIsLame", ".vbs");
877
            // ATTN if the VM is not terminated normally, the file won't be deleted
878
            // ATTN if the VM is not terminated normally, the file won't be deleted
878
            // perhaps a thread to delete the file after a certain amount of time
879
            // perhaps a thread to delete the file after a certain amount of time
879
            shortcutFile.deleteOnExit();
880
            shortcutFile.deleteOnExit();
880
            files.put(url, shortcutFile);
881
            files.put(url, shortcutFile);
881
            try (final InputStream stream = url.openStream(); final FileOutputStream out = new FileOutputStream(shortcutFile);) {
882
            try (final InputStream stream = url.openStream(); final FileOutputStream out = new FileOutputStream(shortcutFile);) {
882
                StreamUtils.copy(stream, out);
883
                StreamUtils.copy(stream, out);
883
            }
884
            }
884
        } else
885
        } else
885
            shortcutFile = currentFile;
886
            shortcutFile = currentFile;
886
        return shortcutFile;
887
        return shortcutFile;
887
    }
888
    }
888
 
889
 
889
    /**
890
    /**
890
     * Create a symbolic link from <code>link</code> to <code>target</code>.
891
     * Create a symbolic link from <code>link</code> to <code>target</code>.
891
     * 
892
     * 
892
     * @param target the target of the link, eg ".".
893
     * @param target the target of the link, eg ".".
893
     * @param link the file to create or replace, eg "l".
894
     * @param link the file to create or replace, eg "l".
894
     * @return the link if the creation was successfull, <code>null</code> otherwise, eg "l.LNK".
895
     * @return the link if the creation was successfull, <code>null</code> otherwise, eg "l.LNK".
895
     * @throws IOException if an error occurs.
896
     * @throws IOException if an error occurs.
896
     */
897
     */
897
    public static final File ln(final File target, final File link) throws IOException {
898
    public static final File ln(final File target, final File link) throws IOException {
898
        final Process ps;
899
        final Process ps;
899
        final File res;
900
        final File res;
900
        if (OSFamily.getInstance() == OSFamily.Windows) {
901
        if (OSFamily.getInstance() == OSFamily.Windows) {
901
            // using the .vbs since it doesn't depends on cygwin
902
            // using the .vbs since it doesn't depends on cygwin
902
            // and cygwin's ln is weird :
903
            // and cygwin's ln is weird :
903
            // 1. needs CYGWIN=winsymlinks to create a shortcut, but even then "ln -f" doesn't work
904
            // 1. needs CYGWIN=winsymlinks to create a shortcut, but even then "ln -f" doesn't work
904
            // since it tries to delete l instead of l.LNK
905
            // since it tries to delete l instead of l.LNK
905
            // 2. it sets the system flag so "dir" doesn't show the shortcut (unless you add /AS)
906
            // 2. it sets the system flag so "dir" doesn't show the shortcut (unless you add /AS)
906
            // 3. the shortcut is recognized as a symlink thanks to a special attribute that can get
907
            // 3. the shortcut is recognized as a symlink thanks to a special attribute that can get
907
            // lost (e.g. copying in eclipse)
908
            // lost (e.g. copying in eclipse)
908
            ps = startDiscardingOutput("cscript", getShortCutFile().getAbsolutePath(), link.getAbsolutePath(), target.getCanonicalPath());
909
            ps = startDiscardingOutput("cscript", getShortCutFile().getAbsolutePath(), link.getAbsolutePath(), target.getCanonicalPath());
909
            res = new File(link.getParentFile(), link.getName() + ".LNK");
910
            res = new File(link.getParentFile(), link.getName() + ".LNK");
910
        } else {
911
        } else {
911
            final String rel = FileUtils.relative(link.getAbsoluteFile().getParentFile(), target);
912
            final String rel = FileUtils.relative(link.getAbsoluteFile().getParentFile(), target);
912
            // add -f to replace existing links
913
            // add -f to replace existing links
913
            // add -n so that ln -sf aDir anExistantLinkToIt succeed
914
            // add -n so that ln -sf aDir anExistantLinkToIt succeed
914
            final String[] cmdarray = { "ln", "-sfn", rel, link.getAbsolutePath() };
915
            final String[] cmdarray = { "ln", "-sfn", rel, link.getAbsolutePath() };
915
            ps = startDiscardingOutput(cmdarray);
916
            ps = startDiscardingOutput(cmdarray);
916
            res = link;
917
            res = link;
917
        }
918
        }
918
        try {
919
        try {
919
            final int exitValue = ps.waitFor();
920
            final int exitValue = ps.waitFor();
920
            if (exitValue == 0)
921
            if (exitValue == 0)
921
                return res;
922
                return res;
922
            else
923
            else
923
                throw new IOException("Abnormal exit value: " + exitValue);
924
                throw new IOException("Abnormal exit value: " + exitValue);
924
        } catch (InterruptedException e) {
925
        } catch (InterruptedException e) {
925
            throw ExceptionUtils.createExn(IOException.class, "interrupted", e);
926
            throw ExceptionUtils.createExn(IOException.class, "interrupted", e);
926
        }
927
        }
927
    }
928
    }
928
 
929
 
929
    /**
930
    /**
930
     * Resolve a symbolic link or a windows shortcut.
931
     * Resolve a symbolic link or a windows shortcut.
931
     * 
932
     * 
932
     * @param link the shortcut, e.g. shortcut.lnk.
933
     * @param link the shortcut, e.g. shortcut.lnk.
933
     * @return the target of <code>link</code>, <code>null</code> if not found, e.g. target.txt.
934
     * @return the target of <code>link</code>, <code>null</code> if not found, e.g. target.txt.
934
     * @throws IOException if an error occurs.
935
     * @throws IOException if an error occurs.
935
     */
936
     */
936
    public static final File readlink(final File link) throws IOException {
937
    public static final File readlink(final File link) throws IOException {
937
        final Process ps;
938
        final Process ps;
938
        if (OSFamily.getInstance() == OSFamily.Windows) {
939
        if (OSFamily.getInstance() == OSFamily.Windows) {
939
            ps = Runtime.getRuntime().exec(new String[] { "cscript", "//NoLogo", getShortCutFile().getAbsolutePath(), link.getAbsolutePath() });
940
            ps = Runtime.getRuntime().exec(new String[] { "cscript", "//NoLogo", getShortCutFile().getAbsolutePath(), link.getAbsolutePath() });
940
        } else {
941
        } else {
941
            // add -f to canonicalize
942
            // add -f to canonicalize
942
            ps = Runtime.getRuntime().exec(new String[] { "readlink", "-f", link.getAbsolutePath() });
943
            ps = Runtime.getRuntime().exec(new String[] { "readlink", "-f", link.getAbsolutePath() });
943
        }
944
        }
944
        try {
945
        try {
945
            ps.getErrorStream().close();
946
            ps.getErrorStream().close();
946
            final String res;
947
            final String res;
947
            try (final BufferedReader reader = new BufferedReader(new InputStreamReader(ps.getInputStream()));) {
948
            try (final BufferedReader reader = new BufferedReader(new InputStreamReader(ps.getInputStream()));) {
948
                res = reader.readLine();
949
                res = reader.readLine();
949
            }
950
            }
950
            if (ps.waitFor() != 0 || res == null || res.length() == 0)
951
            if (ps.waitFor() != 0 || res == null || res.length() == 0)
951
                return null;
952
                return null;
952
            else
953
            else
953
                return new File(res);
954
                return new File(res);
954
        } catch (InterruptedException e) {
955
        } catch (InterruptedException e) {
955
            throw ExceptionUtils.createExn(IOException.class, "interrupted", e);
956
            throw ExceptionUtils.createExn(IOException.class, "interrupted", e);
956
        }
957
        }
957
    }
958
    }
958
 
959
 
959
    // from guava/src/com/google/common/io/Files.java
960
    // from guava/src/com/google/common/io/Files.java
960
    /** Maximum loop count when creating temp directories. */
961
    /** Maximum loop count when creating temp directories. */
961
    private static final int TEMP_DIR_ATTEMPTS = 10000;
962
    private static final int TEMP_DIR_ATTEMPTS = 10000;
962
 
963
 
963
    /**
964
    /**
964
     * Atomically creates a new directory somewhere beneath the system's temporary directory (as
965
     * Atomically creates a new directory somewhere beneath the system's temporary directory (as
965
     * defined by the {@code java.io.tmpdir} system property), and returns its name.
966
     * defined by the {@code java.io.tmpdir} system property), and returns its name.
966
     * 
967
     * 
967
     * <p>
968
     * <p>
968
     * Use this method instead of {@link File#createTempFile(String, String)} when you wish to
969
     * Use this method instead of {@link File#createTempFile(String, String)} when you wish to
969
     * create a directory, not a regular file. A common pitfall is to call {@code createTempFile},
970
     * create a directory, not a regular file. A common pitfall is to call {@code createTempFile},
970
     * delete the file and create a directory in its place, but this leads a race condition which
971
     * delete the file and create a directory in its place, but this leads a race condition which
971
     * can be exploited to create security vulnerabilities, especially when executable files are to
972
     * can be exploited to create security vulnerabilities, especially when executable files are to
972
     * be written into the directory.
973
     * be written into the directory.
973
     * 
974
     * 
974
     * <p>
975
     * <p>
975
     * This method assumes that the temporary volume is writable, has free inodes and free blocks,
976
     * This method assumes that the temporary volume is writable, has free inodes and free blocks,
976
     * and that it will not be called thousands of times per second.
977
     * and that it will not be called thousands of times per second.
977
     * 
978
     * 
978
     * @param prefix the prefix string to be used in generating the directory's name.
979
     * @param prefix the prefix string to be used in generating the directory's name.
979
     * @return the newly-created directory.
980
     * @return the newly-created directory.
980
     * @throws IllegalStateException if the directory could not be created.
981
     * @throws IllegalStateException if the directory could not be created.
981
     * @deprecated use
982
     * @deprecated use
982
     *             {@link Files#createTempDirectory(String, java.nio.file.attribute.FileAttribute...)}
983
     *             {@link Files#createTempDirectory(String, java.nio.file.attribute.FileAttribute...)}
983
     */
984
     */
984
    public static File createTempDir(final String prefix) {
985
    public static File createTempDir(final String prefix) {
985
        final File baseDir = new File(System.getProperty("java.io.tmpdir"));
986
        final File baseDir = new File(System.getProperty("java.io.tmpdir"));
986
        final String baseName = prefix + System.currentTimeMillis() + "-";
987
        final String baseName = prefix + System.currentTimeMillis() + "-";
987
 
988
 
988
        for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
989
        for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
989
            final File tempDir = new File(baseDir, baseName + counter);
990
            final File tempDir = new File(baseDir, baseName + counter);
990
            if (tempDir.mkdir()) {
991
            if (tempDir.mkdir()) {
991
                return tempDir;
992
                return tempDir;
992
            }
993
            }
993
        }
994
        }
994
        throw new IllegalStateException("Failed to create directory within " + TEMP_DIR_ATTEMPTS + " attempts (tried " + baseName + "0 to " + baseName + (TEMP_DIR_ATTEMPTS - 1) + ')');
995
        throw new IllegalStateException("Failed to create directory within " + TEMP_DIR_ATTEMPTS + " attempts (tried " + baseName + "0 to " + baseName + (TEMP_DIR_ATTEMPTS - 1) + ')');
995
    }
996
    }
996
 
997
 
997
    /**
998
    /**
998
     * Tries to open the passed file as if it were graphically opened by the current user (respect
999
     * Tries to open the passed file as if it were graphically opened by the current user (respect
999
     * user's "open with"). If a native way to open the file can't be found, tries the passed list
1000
     * user's "open with"). If a native way to open the file can't be found, tries the passed list
1000
     * of executables.
1001
     * of executables.
1001
     * 
1002
     * 
1002
     * @param f the file to open.
1003
     * @param f the file to open.
1003
     * @param executables a list of executables to try, e.g. ["ooffice", "soffice"].
1004
     * @param executables a list of executables to try, e.g. ["ooffice", "soffice"].
1004
     * @throws IOException if the file can't be opened.
1005
     * @throws IOException if the file can't be opened.
1005
     */
1006
     */
1006
    public static final void open(File f, String[] executables) throws IOException {
1007
    public static final void open(File f, String[] executables) throws IOException {
1007
        if (!f.exists()) {
1008
        if (!f.exists()) {
1008
            throw new FileNotFoundException(f.getAbsolutePath() + " not found");
1009
            throw new FileNotFoundException(f.getAbsolutePath() + " not found");
1009
        }
1010
        }
1010
        try {
1011
        try {
1011
            openNative(f);
1012
            openNative(f);
1012
        } catch (IOException exn) {
1013
        } catch (IOException exn) {
1013
            for (int i = 0; i < executables.length; i++) {
1014
            for (int i = 0; i < executables.length; i++) {
1014
                final String executable = executables[i];
1015
                final String executable = executables[i];
1015
                try {
1016
                try {
1016
                    startDiscardingOutput(executable, f.getCanonicalPath());
1017
                    startDiscardingOutput(executable, f.getCanonicalPath());
1017
                    return;
1018
                    return;
1018
                } catch (IOException e) {
1019
                } catch (IOException e) {
1019
                    exn.addSuppressed(new IOException("unable to open with " + executable, e));
1020
                    exn.addSuppressed(new IOException("unable to open with " + executable, e));
1020
                    // try the next one
1021
                    // try the next one
1021
                }
1022
                }
1022
            }
1023
            }
1023
            throw new IOException("unable to open " + f, exn);
1024
            throw new IOException("unable to open " + f, exn);
1024
        }
1025
        }
1025
    }
1026
    }
1026
 
1027
 
1027
    /**
1028
    /**
1028
     * Open the passed file as if it were graphically opened by the current user (user's "open
1029
     * Open the passed file as if it were graphically opened by the current user (user's "open
1029
     * with").
1030
     * with").
1030
     * 
1031
     * 
1031
     * @param f the file to open.
1032
     * @param f the file to open.
1032
     * @throws IOException if f couldn't be opened.
1033
     * @throws IOException if f couldn't be opened.
1033
     */
1034
     */
1034
    private static final void openNative(File f) throws IOException {
1035
    private static final void openNative(File f) throws IOException {
1035
        openNative(f.getCanonicalPath());
1036
        openNative(f.getCanonicalPath());
1036
    }
1037
    }
1037
 
1038
 
1038
    private static final void openNative(URI uri) throws IOException {
1039
    private static final void openNative(URI uri) throws IOException {
1039
        openNative(uri.toASCIIString());
1040
        openNative(uri.toASCIIString());
1040
    }
1041
    }
1041
 
1042
 
1042
    private static final void openNative(String param) throws IOException {
1043
    private static final void openNative(String param) throws IOException {
1043
        final OSFamily os = OSFamily.getInstance();
1044
        final OSFamily os = OSFamily.getInstance();
1044
        final String[] cmdarray;
1045
        final String[] cmdarray;
1045
        if (os == OSFamily.Windows) {
1046
        if (os == OSFamily.Windows) {
1046
            cmdarray = new String[] { "cmd", "/c", "start", "\"\"", param };
1047
            cmdarray = new String[] { "cmd", "/c", "start", "\"\"", param };
1047
        } else if (os == OSFamily.Mac) {
1048
        } else if (os == OSFamily.Mac) {
1048
            cmdarray = new String[] { "open", param };
1049
            cmdarray = new String[] { "open", param };
1049
        } else if (os instanceof Unix) {
1050
        } else if (os instanceof Unix) {
1050
            cmdarray = new String[] { "xdg-open", param };
1051
            cmdarray = new String[] { "xdg-open", param };
1051
        } else {
1052
        } else {
1052
            throw new IOException("unknown way to open " + param);
1053
            throw new IOException("unknown way to open " + param);
1053
        }
1054
        }
1054
        try {
1055
        try {
1055
            final Process ps = startDiscardingOutput(cmdarray);
1056
            final Process ps = startDiscardingOutput(cmdarray);
1056
            // can wait since the command return as soon as the native application is launched
1057
            // can wait since the command return as soon as the native application is launched
1057
            // (i.e. this won't wait 30s for OpenOffice)
1058
            // (i.e. this won't wait 30s for OpenOffice)
1058
            final int res = ps.waitFor();
1059
            final int res = ps.waitFor();
1059
            if (res != 0)
1060
            if (res != 0)
1060
                throw new IOException("error (" + res + ") executing " + Arrays.asList(cmdarray));
1061
                throw new IOException("error (" + res + ") executing " + Arrays.asList(cmdarray));
1061
        } catch (InterruptedException e) {
1062
        } catch (InterruptedException e) {
1062
            throw ExceptionUtils.createExn(IOException.class, "interrupted waiting for " + Arrays.asList(cmdarray), e);
1063
            throw ExceptionUtils.createExn(IOException.class, "interrupted waiting for " + Arrays.asList(cmdarray), e);
1063
        }
1064
        }
1064
    }
1065
    }
1065
 
1066
 
1066
    private static final Process startDiscardingOutput(final String... command) throws IOException {
1067
    private static final Process startDiscardingOutput(final String... command) throws IOException {
1067
        return startDiscardingOutput(new ProcessBuilder(command));
1068
        return startDiscardingOutput(new ProcessBuilder(command));
1068
    }
1069
    }
1069
 
1070
 
1070
    private static final Process startDiscardingOutput(final ProcessBuilder pb) throws IOException {
1071
    private static final Process startDiscardingOutput(final ProcessBuilder pb) throws IOException {
1071
        final Process res = pb.redirectOutput(ProcessStreams.DISCARD).redirectError(ProcessStreams.DISCARD).start();
1072
        final Process res = pb.redirectOutput(ProcessStreams.DISCARD).redirectError(ProcessStreams.DISCARD).start();
1072
        res.getOutputStream().close();
1073
        res.getOutputStream().close();
1073
        return res;
1074
        return res;
1074
    }
1075
    }
1075
 
1076
 
1076
    static final boolean gnomeRunning() {
1077
    static final boolean gnomeRunning() {
1077
        try {
1078
        try {
1078
            final Process ps = startDiscardingOutput("pgrep", "-u", System.getProperty("user.name"), "nautilus");
1079
            final Process ps = startDiscardingOutput("pgrep", "-u", System.getProperty("user.name"), "nautilus");
1079
            // no need for output, use exit status
1080
            // no need for output, use exit status
1080
            return ps.waitFor() == 0;
1081
            return ps.waitFor() == 0;
1081
        } catch (Exception e) {
1082
        } catch (Exception e) {
1082
            return false;
1083
            return false;
1083
        }
1084
        }
1084
    }
1085
    }
1085
 
1086
 
1086
    public static final String XML_TYPE = "text/xml";
1087
    public static final String XML_TYPE = "text/xml";
1087
    private static final Map<String, String> ext2mime;
1088
    private static final Map<String, String> ext2mime;
1088
    private static final SetMap<String, String> mime2ext;
1089
    private static final SetMap<String, String> mime2ext;
1089
 
1090
 
1090
    static {
1091
    static {
1091
        mime2ext = new SetMap<String, String>(Mode.NULL_FORBIDDEN);
1092
        mime2ext = new SetMap<String, String>(Mode.NULL_FORBIDDEN);
1092
        mime2ext.putCollection(XML_TYPE, ".xml");
1093
        mime2ext.putCollection(XML_TYPE, ".xml");
1093
        mime2ext.putCollection("image/jpeg", ".jpg", ".jpeg");
1094
        mime2ext.putCollection("image/jpeg", ".jpg", ".jpeg");
1094
        mime2ext.putCollection("image/png", ".png");
1095
        mime2ext.putCollection("image/png", ".png");
1095
        mime2ext.putCollection("image/tiff", ".tiff", ".tif");
1096
        mime2ext.putCollection("image/tiff", ".tiff", ".tif");
1096
        mime2ext.putCollection("application/pdf", ".pdf");
1097
        mime2ext.putCollection("application/pdf", ".pdf");
1097
 
1098
 
1098
        mime2ext.putCollection("application/vnd.oasis.opendocument.spreadsheet", ".ods");
1099
        mime2ext.putCollection("application/vnd.oasis.opendocument.spreadsheet", ".ods");
1099
        mime2ext.putCollection("application/vnd.oasis.opendocument.text", ".odt");
1100
        mime2ext.putCollection("application/vnd.oasis.opendocument.text", ".odt");
1100
        mime2ext.putCollection("application/vnd.oasis.opendocument.presentation", ".odp");
1101
        mime2ext.putCollection("application/vnd.oasis.opendocument.presentation", ".odp");
1101
        mime2ext.putCollection("application/vnd.oasis.opendocument.graphics", ".odg");
1102
        mime2ext.putCollection("application/vnd.oasis.opendocument.graphics", ".odg");
1102
 
1103
 
1103
        ext2mime = new HashMap<String, String>();
1104
        ext2mime = new HashMap<String, String>();
1104
        for (final Entry<String, Set<String>> e : mime2ext.entrySet()) {
1105
        for (final Entry<String, Set<String>> e : mime2ext.entrySet()) {
1105
            final String m = e.getKey();
1106
            final String m = e.getKey();
1106
            for (final String ext : e.getValue()) {
1107
            for (final String ext : e.getValue()) {
1107
                if (ext2mime.put(ext, m) != null)
1108
                if (ext2mime.put(ext, m) != null)
1108
                    Log.get().info("Duplicate extension : " + ext);
1109
                    Log.get().info("Duplicate extension : " + ext);
1109
            }
1110
            }
1110
        }
1111
        }
1111
    }
1112
    }
1112
 
1113
 
1113
    /**
1114
    /**
1114
     * Try to guess the media type of the passed file name (see
1115
     * Try to guess the media type of the passed file name (see
1115
     * <a href="http://www.iana.org/assignments/media-types">iana</a>).
1116
     * <a href="http://www.iana.org/assignments/media-types">iana</a>).
1116
     * 
1117
     * 
1117
     * @param fname a file name.
1118
     * @param fname a file name.
1118
     * @return its mime type.
1119
     * @return its mime type.
1119
     */
1120
     */
1120
    public static final String findMimeType(String fname) {
1121
    public static final String findMimeType(String fname) {
1121
        for (final Map.Entry<String, String> e : ext2mime.entrySet()) {
1122
        for (final Map.Entry<String, String> e : ext2mime.entrySet()) {
1122
            if (fname.toLowerCase().endsWith(e.getKey()))
1123
            if (fname.toLowerCase().endsWith(e.getKey()))
1123
                return e.getValue();
1124
                return e.getValue();
1124
        }
1125
        }
1125
        return null;
1126
        return null;
1126
    }
1127
    }
1127
 
1128
 
1128
    public static final Set<String> getExtensionsFromMimeType(final String mimetype) {
1129
    public static final Set<String> getExtensionsFromMimeType(final String mimetype) {
1129
        return mime2ext.get(mimetype);
1130
        return mime2ext.get(mimetype);
1130
    }
1131
    }
1131
 
1132
 
1132
    /**
1133
    /**
1133
     * Return the string after the last dot.
1134
     * Return the string after the last dot.
1134
     * 
1135
     * 
1135
     * @param fname a name, e.g. "test.odt" or "sans".
1136
     * @param fname a name, e.g. "test.odt" or "sans".
1136
     * @return the extension, e.g. "odt" or <code>null</code>.
1137
     * @return the extension, e.g. "odt" or <code>null</code>.
1137
     */
1138
     */
1138
    public static final String getExtension(String fname) {
1139
    public static final String getExtension(String fname) {
1139
        return getExtension(fname, false);
1140
        return getExtension(fname, false);
1140
    }
1141
    }
1141
 
1142
 
1142
    public static final String getExtension(final String fname, final boolean withDot) {
1143
    public static final String getExtension(final String fname, final boolean withDot) {
1143
        final int lastIndex = fname.lastIndexOf('.');
1144
        final int lastIndex = fname.lastIndexOf('.');
1144
        return lastIndex < 0 ? null : fname.substring(lastIndex + (withDot ? 0 : 1));
1145
        return lastIndex < 0 ? null : fname.substring(lastIndex + (withDot ? 0 : 1));
1145
    }
1146
    }
1146
 
1147
 
1147
    /**
1148
    /**
1148
     * Chars not valid in filenames.
1149
     * Chars not valid in filenames.
1149
     */
1150
     */
1150
    public static final Collection<Character> INVALID_CHARS;
1151
    public static final Collection<Character> INVALID_CHARS;
1151
 
1152
 
1152
    /**
1153
    /**
1153
     * An escaper suitable for producing valid filenames.
1154
     * An escaper suitable for producing valid filenames.
1154
     */
1155
     */
1155
    public static final Escaper FILENAME_ESCAPER = new StringUtils.Escaper('\'', 'Q');
1156
    public static final Escaper FILENAME_ESCAPER = new StringUtils.Escaper('\'', 'Q');
1156
 
1157
 
1157
    static private final String WS = "\\p{javaWhitespace}";
1158
    static private final String WS = "\\p{javaWhitespace}";
1158
    static private final Pattern WS_PATTERN = Pattern.compile(WS + "+");
1159
    static private final Pattern WS_PATTERN = Pattern.compile(WS + "+");
1159
    static private final Pattern CONTROL_PATTERN = Pattern.compile("[\\p{IsCc}\\p{IsCf}&&[^" + WS + "]]+");
1160
    static private final Pattern CONTROL_PATTERN = Pattern.compile("[\\p{IsCc}\\p{IsCf}&&[^" + WS + "]]+");
1160
    static private final Pattern INVALID_CHARS_PATTERN;
1161
    static private final Pattern INVALID_CHARS_PATTERN;
1161
 
1162
 
1162
    static {
1163
    static {
1163
        // from windows explorer
1164
        // from windows explorer
1164
        // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
1165
        // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
1165
        // Naming Files, Paths, and Namespaces
1166
        // Naming Files, Paths, and Namespaces
1166
        // on Mac only '/' and ':', on Linux only '/'
1167
        // on Mac only '/' and ':', on Linux only '/'
1167
        FILENAME_ESCAPER.add('"', 'D').add(':', 'C').add('/', 'S').add('\\', 'A');
1168
        FILENAME_ESCAPER.add('"', 'D').add(':', 'C').add('/', 'S').add('\\', 'A');
1168
        FILENAME_ESCAPER.add('<', 'L').add('>', 'G').add('*', 'R').add('|', 'P').add('?', 'M');
1169
        FILENAME_ESCAPER.add('<', 'L').add('>', 'G').add('*', 'R').add('|', 'P').add('?', 'M');
1169
        INVALID_CHARS = FILENAME_ESCAPER.getEscapedChars();
1170
        INVALID_CHARS = FILENAME_ESCAPER.getEscapedChars();
1170
        INVALID_CHARS_PATTERN = Pattern.compile("[" + CollectionUtils.join(INVALID_CHARS, "") + "]");
1171
        INVALID_CHARS_PATTERN = Pattern.compile("[" + CollectionUtils.join(INVALID_CHARS, "") + "]");
1171
    }
1172
    }
1172
 
1173
 
1173
    /**
1174
    /**
1174
     * Sanitize a name. Remove control characters, trim, and replace {@link #INVALID_CHARS} by '_'.
1175
     * Sanitize a name. Remove control characters, trim, and replace {@link #INVALID_CHARS} by '_'.
1175
     * 
1176
     * 
1176
     * @param name an arbitrary name.
1177
     * @param name an arbitrary name.
1177
     * @return a name suitable for any file system.
1178
     * @return a name suitable for any file system.
1178
     */
1179
     */
1179
    static public final String sanitize(String name) {
1180
    static public final String sanitize(String name) {
1180
        // remove control and format characters (except white spaces)
1181
        // remove control and format characters (except white spaces)
1181
        name = CONTROL_PATTERN.matcher(name).replaceAll("");
1182
        name = CONTROL_PATTERN.matcher(name).replaceAll("");
1182
        // only use one regular space (must be done after removing control characters as if they are
1183
        // only use one regular space (must be done after removing control characters as if they are
1183
        // between spaces we want only one space to remain)
1184
        // between spaces we want only one space to remain)
1184
        name = WS_PATTERN.matcher(name).replaceAll(" ");
1185
        name = WS_PATTERN.matcher(name).replaceAll(" ");
1185
        // leading and trailing spaces are hard to see (and illegal in Explorer)
1186
        // leading and trailing spaces are hard to see (and illegal in Explorer)
1186
        name = name.trim();
1187
        name = name.trim();
1187
 
1188
 
1188
        // replace all invalid characters with _
1189
        // replace all invalid characters with _
1189
        name = INVALID_CHARS_PATTERN.matcher(name).replaceAll("_");
1190
        name = INVALID_CHARS_PATTERN.matcher(name).replaceAll("_");
1190
 
1191
 
1191
        return name;
1192
        return name;
1192
    }
1193
    }
1193
 
1194
 
1194
    public static final FileFilter DIR_FILTER = new FileFilter() {
1195
    public static final FileFilter DIR_FILTER = new FileFilter() {
1195
        @Override
1196
        @Override
1196
        public boolean accept(File f) {
1197
        public boolean accept(File f) {
1197
            return f.isDirectory();
1198
            return f.isDirectory();
1198
        }
1199
        }
1199
    };
1200
    };
1200
    public static final FileFilter REGULAR_FILE_FILTER = new FileFilter() {
1201
    public static final FileFilter REGULAR_FILE_FILTER = new FileFilter() {
1201
        @Override
1202
        @Override
1202
        public boolean accept(File f) {
1203
        public boolean accept(File f) {
1203
            return f.isFile();
1204
            return f.isFile();
1204
        }
1205
        }
1205
    };
1206
    };
1206
    public static final Filter<Path> DIR_PATH_FILTER = new DirectoryStream.Filter<Path>() {
1207
    public static final Filter<Path> DIR_PATH_FILTER = new DirectoryStream.Filter<Path>() {
1207
        @Override
1208
        @Override
1208
        public boolean accept(Path entry) throws IOException {
1209
        public boolean accept(Path entry) throws IOException {
1209
            return Files.isDirectory(entry, LinkOption.NOFOLLOW_LINKS);
1210
            return Files.isDirectory(entry, LinkOption.NOFOLLOW_LINKS);
1210
        }
1211
        }
1211
    };
1212
    };
1212
 
1213
 
1213
    /**
1214
    /**
1214
     * Return a filter that select regular files ending in <code>ext</code>.
1215
     * Return a filter that select regular files ending in <code>ext</code>.
1215
     * 
1216
     * 
1216
     * @param ext the end of the name, eg ".xml".
1217
     * @param ext the end of the name, eg ".xml".
1217
     * @return the corresponding filter.
1218
     * @return the corresponding filter.
1218
     */
1219
     */
1219
    public static final FileFilter createEndFileFilter(final String ext) {
1220
    public static final FileFilter createEndFileFilter(final String ext) {
1220
        return new FileFilter() {
1221
        return new FileFilter() {
1221
            @Override
1222
            @Override
1222
            public boolean accept(File f) {
1223
            public boolean accept(File f) {
1223
                return f.isFile() && f.getName().endsWith(ext);
1224
                return f.isFile() && f.getName().endsWith(ext);
1224
            }
1225
            }
1225
        };
1226
        };
1226
    }
1227
    }
1227
 
1228
 
1228
    /**
1229
    /**
1229
     * How to merge the group and others portion of permissions for non-POSIX FS.
1230
     * How to merge the group and others portion of permissions for non-POSIX FS.
1230
     * 
1231
     * 
1231
     * @author sylvain
1232
     * @author sylvain
1232
     * @see FileUtils#setFilePermissionsFromPOSIX(File, String, GroupAndOthers)
1233
     * @see FileUtils#setFilePermissionsFromPOSIX(File, String, GroupAndOthers)
1233
     */
1234
     */
1234
    public static enum GroupAndOthers {
1235
    public static enum GroupAndOthers {
1235
        REQUIRE_SAME {
1236
        REQUIRE_SAME {
1236
            @Override
1237
            @Override
1237
            protected Set<Permission> getNonEqual(Set<Permission> groupPerms, Set<Permission> otherPerms) {
1238
            protected Set<Permission> getNonEqual(Set<Permission> groupPerms, Set<Permission> otherPerms) {
1238
                throw new IllegalArgumentException("Different permissions : " + groupPerms + " != " + otherPerms);
1239
                throw new IllegalArgumentException("Different permissions : " + groupPerms + " != " + otherPerms);
1239
            }
1240
            }
1240
        },
1241
        },
1241
        PERMISSIVE {
1242
        PERMISSIVE {
1242
            @Override
1243
            @Override
1243
            protected Set<Permission> getNonEqual(Set<Permission> groupPerms, Set<Permission> otherPerms) {
1244
            protected Set<Permission> getNonEqual(Set<Permission> groupPerms, Set<Permission> otherPerms) {
1244
                final EnumSet<Permission> res = EnumSet.noneOf(Permission.class);
1245
                final EnumSet<Permission> res = EnumSet.noneOf(Permission.class);
1245
                res.addAll(groupPerms);
1246
                res.addAll(groupPerms);
1246
                res.addAll(otherPerms);
1247
                res.addAll(otherPerms);
1247
                return res;
1248
                return res;
1248
            }
1249
            }
1249
        },
1250
        },
1250
        OTHERS {
1251
        OTHERS {
1251
            @Override
1252
            @Override
1252
            protected Set<Permission> getNonEqual(Set<Permission> groupPerms, Set<Permission> otherPerms) {
1253
            protected Set<Permission> getNonEqual(Set<Permission> groupPerms, Set<Permission> otherPerms) {
1253
                return otherPerms;
1254
                return otherPerms;
1254
            }
1255
            }
1255
        },
1256
        },
1256
        RESTRICTIVE {
1257
        RESTRICTIVE {
1257
            @Override
1258
            @Override
1258
            protected Set<Permission> getNonEqual(Set<Permission> groupPerms, Set<Permission> otherPerms) {
1259
            protected Set<Permission> getNonEqual(Set<Permission> groupPerms, Set<Permission> otherPerms) {
1259
                final EnumSet<Permission> res = EnumSet.allOf(Permission.class);
1260
                final EnumSet<Permission> res = EnumSet.allOf(Permission.class);
1260
                res.retainAll(groupPerms);
1261
                res.retainAll(groupPerms);
1261
                res.retainAll(otherPerms);
1262
                res.retainAll(otherPerms);
1262
                return res;
1263
                return res;
1263
            }
1264
            }
1264
        };
1265
        };
1265
 
1266
 
1266
        public final Set<Permission> getPermissions(final Set<Permission> groupPerms, final Set<Permission> otherPerms) {
1267
        public final Set<Permission> getPermissions(final Set<Permission> groupPerms, final Set<Permission> otherPerms) {
1267
            if (groupPerms.equals(otherPerms)) {
1268
            if (groupPerms.equals(otherPerms)) {
1268
                return groupPerms;
1269
                return groupPerms;
1269
            } else {
1270
            } else {
1270
                return getNonEqual(groupPerms, otherPerms);
1271
                return getNonEqual(groupPerms, otherPerms);
1271
            }
1272
            }
1272
        }
1273
        }
1273
 
1274
 
1274
        public final Set<Permission> getPermissions(final String posixPerms) {
1275
        public final Set<Permission> getPermissions(final String posixPerms) {
1275
            final Set<Permission> groupPerms = Permission.fromString(posixPerms.substring(3, 6));
1276
            final Set<Permission> groupPerms = Permission.fromString(posixPerms.substring(3, 6));
1276
            final Set<Permission> otherPerms = Permission.fromString(posixPerms.substring(6, 9));
1277
            final Set<Permission> otherPerms = Permission.fromString(posixPerms.substring(6, 9));
1277
            return this.getPermissions(groupPerms, otherPerms);
1278
            return this.getPermissions(groupPerms, otherPerms);
1278
        }
1279
        }
1279
 
1280
 
1280
        protected abstract Set<Permission> getNonEqual(final Set<Permission> groupPerms, final Set<Permission> otherPerms);
1281
        protected abstract Set<Permission> getNonEqual(final Set<Permission> groupPerms, final Set<Permission> otherPerms);
1281
 
1282
 
1282
    }
1283
    }
1283
 
1284
 
1284
    public static final String setPermissions(final Path p, final String posixPerms) throws IOException {
1285
    public static final String setPermissions(final Path p, final String posixPerms) throws IOException {
1285
        return setPermissions(p, posixPerms, GroupAndOthers.RESTRICTIVE);
1286
        return setPermissions(p, posixPerms, GroupAndOthers.RESTRICTIVE);
1286
    }
1287
    }
1287
 
1288
 
1288
    /**
1289
    /**
1289
     * Use {@link PosixFileAttributeView#setPermissions(Set)} if possible, otherwise use
1290
     * Use {@link PosixFileAttributeView#setPermissions(Set)} if possible, otherwise use
1290
     * {@link #setFilePermissionsFromPOSIX(File, String, GroupAndOthers)}.
1291
     * {@link #setFilePermissionsFromPOSIX(File, String, GroupAndOthers)}.
1291
     * 
1292
     * 
1292
     * @param p the path to change.
1293
     * @param p the path to change.
1293
     * @param posixPerms the new permissions to apply.
1294
     * @param posixPerms the new permissions to apply.
1294
     * @param groupAndOthers only for non-POSIX FS, how to merge group and others portion.
1295
     * @param groupAndOthers only for non-POSIX FS, how to merge group and others portion.
1295
     * @return the permission applied, 9 characters for POSIX, 6 for non-POSIX (i.e. 3 for owner, 3
1296
     * @return the permission applied, 9 characters for POSIX, 6 for non-POSIX (i.e. 3 for owner, 3
1296
     *         for the rest), <code>null</code> if some permissions couldn't be applied (only on
1297
     *         for the rest), <code>null</code> if some permissions couldn't be applied (only on
1297
     *         non-POSIX).
1298
     *         non-POSIX).
1298
     * @throws IOException if permissions couldn't be applied.
1299
     * @throws IOException if permissions couldn't be applied.
1299
     */
1300
     */
1300
    public static final String setPermissions(final Path p, final String posixPerms, final GroupAndOthers groupAndOthers) throws IOException {
1301
    public static final String setPermissions(final Path p, final String posixPerms, final GroupAndOthers groupAndOthers) throws IOException {
1301
        final String res;
1302
        final String res;
1302
        final PosixFileAttributeView view = Files.getFileAttributeView(p, PosixFileAttributeView.class);
1303
        final PosixFileAttributeView view = Files.getFileAttributeView(p, PosixFileAttributeView.class);
1303
        if (view != null) {
1304
        if (view != null) {
1304
            view.setPermissions(PosixFilePermissions.fromString(posixPerms));
1305
            view.setPermissions(PosixFilePermissions.fromString(posixPerms));
1305
            res = posixPerms;
1306
            res = posixPerms;
1306
        } else {
1307
        } else {
1307
            // final Set<Permission> notOwnerPerms = setFilePermissions(p.toFile(), pfp,
1308
            // final Set<Permission> notOwnerPerms = setFilePermissions(p.toFile(), pfp,
1308
            // groupAndOthers);
1309
            // groupAndOthers);
1309
            final Set<Permission> notOwnerPerms = setFilePermissionsFromPOSIX(p.toFile(), posixPerms, groupAndOthers);
1310
            final Set<Permission> notOwnerPerms = setFilePermissionsFromPOSIX(p.toFile(), posixPerms, groupAndOthers);
1310
            res = notOwnerPerms == null ? null : posixPerms.substring(0, 3) + Permission.get3chars(notOwnerPerms);
1311
            res = notOwnerPerms == null ? null : posixPerms.substring(0, 3) + Permission.get3chars(notOwnerPerms);
1311
        }
1312
        }
1312
        return res;
1313
        return res;
1313
    }
1314
    }
1314
 
1315
 
1315
    public static final Set<Permission> setFilePermissionsFromPOSIX(final File f, final String posixPerms) {
1316
    public static final Set<Permission> setFilePermissionsFromPOSIX(final File f, final String posixPerms) {
1316
        return setFilePermissionsFromPOSIX(f, posixPerms, GroupAndOthers.RESTRICTIVE);
1317
        return setFilePermissionsFromPOSIX(f, posixPerms, GroupAndOthers.RESTRICTIVE);
1317
    }
1318
    }
1318
 
1319
 
1319
    /**
1320
    /**
1320
     * This method doesn't need POSIX but must merge permissions before applying them.
1321
     * This method doesn't need POSIX but must merge permissions before applying them.
1321
     * 
1322
     * 
1322
     * @param f the file to change.
1323
     * @param f the file to change.
1323
     * @param posixPerms the POSIX permissions to merge.
1324
     * @param posixPerms the POSIX permissions to merge.
1324
     * @param groupAndOthers how to merge.
1325
     * @param groupAndOthers how to merge.
1325
     * @return the merged permissions for the "not owner" portion, or <code>null</code> if some
1326
     * @return the merged permissions for the "not owner" portion, or <code>null</code> if some
1326
     *         permissions couldn't be set.
1327
     *         permissions couldn't be set.
1327
     * @see #setFilePermissions(File, Set, Set)
1328
     * @see #setFilePermissions(File, Set, Set)
1328
     */
1329
     */
1329
    public static final Set<Permission> setFilePermissionsFromPOSIX(final File f, final String posixPerms, final GroupAndOthers groupAndOthers) {
1330
    public static final Set<Permission> setFilePermissionsFromPOSIX(final File f, final String posixPerms, final GroupAndOthers groupAndOthers) {
1330
        if (posixPerms.length() != 9)
1331
        if (posixPerms.length() != 9)
1331
            throw new IllegalArgumentException("Invalid mode : " + posixPerms);
1332
            throw new IllegalArgumentException("Invalid mode : " + posixPerms);
1332
        final Set<Permission> ownerPerms = Permission.fromString(posixPerms.substring(0, 3));
1333
        final Set<Permission> ownerPerms = Permission.fromString(posixPerms.substring(0, 3));
1333
        final Set<Permission> notOwnerPerms = groupAndOthers.getPermissions(posixPerms);
1334
        final Set<Permission> notOwnerPerms = groupAndOthers.getPermissions(posixPerms);
1334
        assert notOwnerPerms != null;
1335
        assert notOwnerPerms != null;
1335
        final boolean success = setFilePermissions(f, ownerPerms, notOwnerPerms);
1336
        final boolean success = setFilePermissions(f, ownerPerms, notOwnerPerms);
1336
        return success ? notOwnerPerms : null;
1337
        return success ? notOwnerPerms : null;
1337
    }
1338
    }
1338
 
1339
 
1339
    /**
1340
    /**
1340
     * Use {@link File} methods to set permissions. This works everywhere but group and others are
1341
     * Use {@link File} methods to set permissions. This works everywhere but group and others are
1341
     * treated as the same.
1342
     * treated as the same.
1342
     * 
1343
     * 
1343
     * @param f the file to change.
1344
     * @param f the file to change.
1344
     * @param owner the permissions for the owner.
1345
     * @param owner the permissions for the owner.
1345
     * @param notOwner the permissions for not the owner.
1346
     * @param notOwner the permissions for not the owner.
1346
     * @return <code>true</code> if all asked permissions were set.
1347
     * @return <code>true</code> if all asked permissions were set.
1347
     * @see File#setReadable(boolean, boolean)
1348
     * @see File#setReadable(boolean, boolean)
1348
     * @see File#setWritable(boolean, boolean)
1349
     * @see File#setWritable(boolean, boolean)
1349
     * @see File#setExecutable(boolean, boolean)
1350
     * @see File#setExecutable(boolean, boolean)
1350
     */
1351
     */
1351
    public static final boolean setFilePermissions(final File f, final Set<Permission> owner, final Set<Permission> notOwner) {
1352
    public static final boolean setFilePermissions(final File f, final Set<Permission> owner, final Set<Permission> notOwner) {
1352
        boolean res = setFilePermissions(f, notOwner, false);
1353
        boolean res = setFilePermissions(f, notOwner, false);
1353
        if (!owner.equals(notOwner)) {
1354
        if (!owner.equals(notOwner)) {
1354
            res &= setFilePermissions(f, owner, true);
1355
            res &= setFilePermissions(f, owner, true);
1355
        }
1356
        }
1356
        return res;
1357
        return res;
1357
    }
1358
    }
1358
 
1359
 
1359
    public static final boolean setFilePermissions(final File f, final Set<Permission> perms, final boolean ownerOnly) {
1360
    public static final boolean setFilePermissions(final File f, final Set<Permission> perms, final boolean ownerOnly) {
1360
        boolean res = f.setReadable(perms.contains(Permission.READ), ownerOnly);
1361
        boolean res = f.setReadable(perms.contains(Permission.READ), ownerOnly);
1361
        res &= f.setWritable(perms.contains(Permission.WRITE), ownerOnly);
1362
        res &= f.setWritable(perms.contains(Permission.WRITE), ownerOnly);
1362
        res &= f.setExecutable(perms.contains(Permission.EXECUTE), ownerOnly);
1363
        res &= f.setExecutable(perms.contains(Permission.EXECUTE), ownerOnly);
1363
        return res;
1364
        return res;
1364
    }
1365
    }
1365
 
1366
 
1366
    public static enum Permission {
1367
    public static enum Permission {
1367
        READ, WRITE, EXECUTE;
1368
        READ, WRITE, EXECUTE;
1368
 
1369
 
1369
        public static final Permission R = READ;
1370
        public static final Permission R = READ;
1370
        public static final Permission W = WRITE;
1371
        public static final Permission W = WRITE;
1371
        public static final Permission X = EXECUTE;
1372
        public static final Permission X = EXECUTE;
1372
        public static final Map<String, Set<Permission>> FROM_STRING = new HashMap<>();
1373
        public static final Map<String, Set<Permission>> FROM_STRING = new HashMap<>();
1373
        public static final Pattern MINUS_PATTERN = Pattern.compile("-+");
1374
        public static final Pattern MINUS_PATTERN = Pattern.compile("-+");
1374
        static {
1375
        static {
1375
            putString("---", Collections.<Permission> emptySet());
1376
            putString("---", Collections.<Permission> emptySet());
1376
            putString("--x", Collections.singleton(EXECUTE));
1377
            putString("--x", Collections.singleton(EXECUTE));
1377
            putString("-w-", Collections.singleton(WRITE));
1378
            putString("-w-", Collections.singleton(WRITE));
1378
            putString("-wx", EnumSet.of(WRITE, EXECUTE));
1379
            putString("-wx", EnumSet.of(WRITE, EXECUTE));
1379
            putString("r--", Collections.singleton(READ));
1380
            putString("r--", Collections.singleton(READ));
1380
            putString("r-x", EnumSet.of(READ, EXECUTE));
1381
            putString("r-x", EnumSet.of(READ, EXECUTE));
1381
            putString("rw-", EnumSet.of(READ, WRITE));
1382
            putString("rw-", EnumSet.of(READ, WRITE));
1382
            putString("rwx", EnumSet.allOf(Permission.class));
1383
            putString("rwx", EnumSet.allOf(Permission.class));
1383
        }
1384
        }
1384
 
1385
 
1385
        static private final void putString(final String str, final EnumSet<Permission> set) {
1386
        static private final void putString(final String str, final EnumSet<Permission> set) {
1386
            putString(str, Collections.unmodifiableSet(set));
1387
            putString(str, Collections.unmodifiableSet(set));
1387
        }
1388
        }
1388
 
1389
 
1389
        static private final void putString(final String str, final Set<Permission> unmodifiableSet) {
1390
        static private final void putString(final String str, final Set<Permission> unmodifiableSet) {
1390
            FROM_STRING.put(str, unmodifiableSet);
1391
            FROM_STRING.put(str, unmodifiableSet);
1391
            FROM_STRING.put(MINUS_PATTERN.matcher(str).replaceAll(""), unmodifiableSet);
1392
            FROM_STRING.put(MINUS_PATTERN.matcher(str).replaceAll(""), unmodifiableSet);
1392
        }
1393
        }
1393
 
1394
 
1394
        static public final Set<Permission> fromString(final String str) {
1395
        static public final Set<Permission> fromString(final String str) {
1395
            final Set<Permission> res = FROM_STRING.get(str);
1396
            final Set<Permission> res = FROM_STRING.get(str);
1396
            if (res == null)
1397
            if (res == null)
1397
                throw new IllegalArgumentException("Invalid string : " + str);
1398
                throw new IllegalArgumentException("Invalid string : " + str);
1398
            return res;
1399
            return res;
1399
        }
1400
        }
1400
 
1401
 
1401
        public static final String get3chars(final Set<Permission> perms) {
1402
        public static final String get3chars(final Set<Permission> perms) {
1402
            return get3chars(perms.contains(READ), perms.contains(WRITE), perms.contains(EXECUTE));
1403
            return get3chars(perms.contains(READ), perms.contains(WRITE), perms.contains(EXECUTE));
1403
        }
1404
        }
1404
 
1405
 
1405
        private static final String get3chars(final boolean read, final boolean write, final boolean exec) {
1406
        private static final String get3chars(final boolean read, final boolean write, final boolean exec) {
1406
            final StringBuilder sb = new StringBuilder(3);
1407
            final StringBuilder sb = new StringBuilder(3);
1407
            sb.append(read ? 'r' : '-');
1408
            sb.append(read ? 'r' : '-');
1408
            sb.append(write ? 'w' : '-');
1409
            sb.append(write ? 'w' : '-');
1409
            sb.append(exec ? 'x' : '-');
1410
            sb.append(exec ? 'x' : '-');
1410
            return sb.toString();
1411
            return sb.toString();
1411
        }
1412
        }
1412
    }
1413
    }
1413
}
1414
}