blob: 9f11db0cca8117f734f3b11cc4dabf2af792f307 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.openide.filesystems;
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SyncFailedException;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLStreamHandler;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.modules.openide.filesystems.declmime.MIMEResolverImpl;
import org.openide.filesystems.FileSystem.AtomicAction;
import org.openide.filesystems.spi.ArchiveRootProvider;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.Parameters;
import org.openide.util.RequestProcessor;
import org.openide.util.BaseUtilities;
import org.openide.util.Lookup;
import org.openide.util.WeakListeners;
import org.openide.util.lookup.Lookups;
import org.openide.util.lookup.ProxyLookup;
import org.openide.util.lookup.implspi.NamedServicesProvider;
/** Common utilities for handling files.
* This is a dummy class; all methods are static.
*/
public final class FileUtil extends Object {
private static final RequestProcessor REFRESH_RP = new RequestProcessor("FileUtil-Refresh-All");//NOI18N
private static RequestProcessor.Task refreshTask = null;
private static final Logger LOG = Logger.getLogger(FileUtil.class.getName());
/** transient attributes which should not be copied
* of type Set<String>
*/
static final Set<String> transientAttributes = new HashSet<String>();
static {
transientAttributes.add("templateWizardURL"); // NOI18N
transientAttributes.add("templateWizardIterator"); // NOI18N
transientAttributes.add("templateWizardDescResource"); // NOI18N
transientAttributes.add("templateCategory"); // NOI18N
transientAttributes.add("instantiatingIterator"); // NOI18N
transientAttributes.add("instantiatingWizardURL"); // NOI18N
transientAttributes.add("SystemFileSystem.localizingBundle"); // NOI18N
transientAttributes.add("SystemFileSystem.icon"); // NOI18N
transientAttributes.add("SystemFileSystem.icon32"); // NOI18N
transientAttributes.add("displayName"); // NOI18N
transientAttributes.add("iconBase"); // NOI18N
transientAttributes.add("position"); // NOI18N
transientAttributes.add(MultiFileObject.WEIGHT_ATTRIBUTE); // NOI18N
}
private static FileSystem diskFileSystem;
static String toDebugString(File file) {
if (file == null) {
return "NULL-ref"; // NOI18N
} else {
return file.getPath() + "(" + file.getClass() + ")"; // NOI18N
}
}
static boolean assertNormalized(File path) {
return assertNormalized(path, false);
}
/**
* @param path Path to compare with its normalized path.
* @param forceIgnoreCase Force case insensitive comparison with normalized
* path.
*
* return True if the path is equal to its normalized path, false otherwise.
*/
static boolean assertNormalized(File path, boolean forceIgnoreCase) {
if (path != null) {
File np = null;
assert path.getClass().getName().startsWith("sun.awt.shell") ||
(!forceIgnoreCase && path.equals(np = FileUtil.normalizeFileCached(path))) ||
(forceIgnoreCase && path.getPath().equalsIgnoreCase((np = FileUtil.normalizeFileCached(path)).getPath())):
"Need to normalize " + toDebugString(path) + " was " + toDebugString(np); //NOI18N
}
return true;
}
private static FileSystem getDiskFileSystemFor(File... files) {
FileSystem fs = getDiskFileSystem();
if (fs == null) {
for (File file : files) {
FileObject fo = toFileObject(file);
fs = getDiskFileSystem();
if (fs != null) {
break;
}
}
}
return fs;
}
private FileUtil() {
}
/**
* Refreshes all necessary filesystems. Not all instances of <code>FileObject</code> are refreshed
* but just those that represent passed <code>files</code> and their children recursively.
* @param files
* @since 7.6
*/
public static void refreshFor(File... files) {
FileSystem fs = getDiskFileSystemFor(files);
if (fs != null) {
try {
fs.getRoot().setAttribute("request_for_refreshing_files_be_aware_this_is_not_public_api", files);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
}
/**
* Refreshes all <code>FileObject</code> that represent files <code>File.listRoots()</code>
* and their children recursively.
* @since 7.7
*/
public static void refreshAll() {
// run just one refreshTask in time and always wait for finish
final RequestProcessor.Task taskToWaitFor; // prevent possible NPE with only refreshTask.waitFinished
synchronized (REFRESH_RP) {
if (refreshTask != null) {
refreshTask.cancel();
} else {
refreshTask = REFRESH_RP.create(new Runnable() {
public void run() {
LOG.fine("refreshAll - started"); //NOI18N
refreshFor(File.listRoots());
try {
getConfigRoot().getFileSystem().refresh(true);
} catch (FileStateInvalidException ex) {
Exceptions.printStackTrace(ex);
} finally {
LOG.fine("refreshAll - finished"); //NOI18N
synchronized (REFRESH_RP) {
refreshTask = null;
}
}
}
});
}
taskToWaitFor = refreshTask;
refreshTask.schedule(0);
LOG.fine("refreshAll - scheduled"); //NOI18N
}
taskToWaitFor.waitFinished();
LOG.fine("refreshAll - finished"); //NOI18N
}
/**
* Registers <code>listener</code> so that it will receive
* <code>FileEvent</code>s from <code>FileSystem</code>s providing instances
* of <code>FileObject</code> convertible to <code>java.io.File</code>.
* @param fcl
* @see #toFileObject
* @since 7.7
*/
public static void addFileChangeListener(FileChangeListener fcl) {
FileSystem fs = getDiskFileSystem();
if (fs == null) {fs = getDiskFileSystemFor(File.listRoots());}
if (fs != null) {
fs.addFileChangeListener(fcl);
}
}
/**
* Unregisters <code>listener</code> so that it will no longer receive
* <code>FileEvent</code>s from <code>FileSystem</code>s providing instances
* of <code>FileObject</code> convertible to <code>java.io.File</code>
* @param fcl
* @see #toFileObject
* @since 7.7
*/
public static void removeFileChangeListener(FileChangeListener fcl) {
FileSystem fs = getDiskFileSystem();
if (fs == null) {fs = getDiskFileSystemFor(File.listRoots());}
if (fs != null) {
fs.removeFileChangeListener(fcl);
}
}
/**
* Adds a listener to changes in a given path. It permits you to listen to a file
* which does not yet exist, or continue listening to it after it is deleted and recreated, etc.
* <br/>
* When given path represents a file ({@code path.isDirectory() == false})
* <ul>
* <li>fileDataCreated event is fired when the file is created</li>
* <li>fileDeleted event is fired when the file is deleted</li>
* <li>fileChanged event is fired when the file is modified</li>
* <li>fileRenamed event is fired when the file is renamed</li>
* <li>fileAttributeChanged is fired when FileObject's attribute is changed</li>
* </ul>
* When given path represents a folder ({@code path.isDirectory() == true})
* <ul>
* <li>fileFolderCreated event is fired when the folder is created or a child folder created</li>
* <li>fileDataCreated event is fired when a child file is created</li>
* <li>fileDeleted event is fired when the folder is deleted or a child file/folder removed</li>
* <li>fileChanged event is fired when a child file is modified</li>
* <li>fileRenamed event is fired when the folder is renamed or a child file/folder is renamed</li>
* <li>fileAttributeChanged is fired when FileObject's attribute is changed</li>
*</ul>
* Can only add a given [listener, path] pair once. However a listener can
* listen to any number of paths. Note that listeners are always held weakly
* - if the listener is collected, it is quietly removed.
*
* @param listener FileChangeListener to listen to changes in path
* @param path File path to listen to (even not existing)
*
* @see FileObject#addFileChangeListener
* @since org.openide.filesystems 7.20
*/
public static void addFileChangeListener(FileChangeListener listener, File path) {
FileChangeImpl.addFileChangeListenerImpl(LOG, listener, path);
}
/**
* Removes a listener to changes in a given path.
* @param listener FileChangeListener to be removed
* @param path File path in which listener was listening
* @throws IllegalArgumentException if listener was not listening to given path
*
* @see FileObject#removeFileChangeListener
* @since org.openide.filesystems 7.20
*/
public static void removeFileChangeListener(FileChangeListener listener, File path) {
FileChangeImpl.removeFileChangeListenerImpl(LOG, listener, path);
}
/**
* Works like {@link #addRecursiveListener(org.openide.filesystems.FileChangeListener, java.io.File, java.io.FileFilter, java.util.concurrent.Callable)
* addRecursiveListener(listener, path, null, null)}.
*
* @param listener FileChangeListener to listen to changes in path
* @param path File path to listen to (even not existing)
*
* @since org.openide.filesystems 7.28
*/
public static void addRecursiveListener(FileChangeListener listener, File path) {
addRecursiveListener(listener, path, null, null);
}
/** Works like {@link #addRecursiveListener(org.openide.filesystems.FileChangeListener, java.io.File, java.io.FileFilter, java.util.concurrent.Callable)
* addRecursiveListener(listener, path, null, stop)}.
*
* @param listener FileChangeListener to listen to changes in path
* @param path File path to listen to (even not existing)
* @param stop an interface to interrupt the process of registering
* the listener. If the <code>call</code> returns true, the process
* of registering the listener is immediately interrupted
*
* @see FileObject#addRecursiveListener
* @since org.openide.filesystems 7.37
*/
public static void addRecursiveListener(FileChangeListener listener, File path, Callable<Boolean> stop) {
addRecursiveListener(listener, path, null, stop);
}
/**
* Adds a listener to changes under given path. It permits you to listen to a file
* which does not yet exist, or continue listening to it after it is deleted and recreated, etc.
* <br/>
* When given path represents a file ({@code path.isDirectory() == false}), this
* code behaves exactly like {@link #addFileChangeListener(org.openide.filesystems.FileChangeListener, java.io.File)}.
* Usually the path shall represent a folder ({@code path.isDirectory() == true})
* <ul>
* <li>fileFolderCreated event is fired when the folder is created or a child folder created</li>
* <li>fileDataCreated event is fired when a child file is created</li>
* <li>fileDeleted event is fired when the folder is deleted or a child file/folder removed</li>
* <li>fileChanged event is fired when a child file is modified</li>
* <li>fileRenamed event is fired when the folder is renamed or a child file/folder is renamed</li>
* <li>fileAttributeChanged is fired when FileObject's attribute is changed</li>
*</ul>
* The above events are delivered for changes in all subdirectories (recursively).
* It is guaranteed that with each change at least one event is generated.
* For example adding a folder does not notify about content of the folder,
* hence one event is delivered.
*
* Can only add a given [listener, path] pair once. However a listener can
* listen to any number of paths. Note that listeners are always held weakly
* - if the listener is collected, it is quietly removed.
*
* <div class="nonnormative">
* As registering of the listener can take a long time, especially on deep
* hierarchies, it is possible provide a callback <code>stop</code>.
* This stop object is guaranteed to be called once per every folder on the
* default (when masterfs module is included) implemention. If the call
* to <code>stop.call()</code> returns true, then the registration of
* next recursive items is interrupted. The listener may or may not get
* some events from already registered folders.
* </div>
*
* Those who provide {@link FileFilter recurseInto} callback can prevent
* the system to enter, and register operating system level listeners
* to certain subtrees under the provided <code>path</code>. This does
* not prevent delivery of changes, if they are made via the filesystem API.
* External changes however will not be detected.
*
* @param listener FileChangeListener to listen to changes in path
* @param path File path to listen to (even not existing)
* @param stop an interface to interrupt the process of registering
* the listener. If the <code>call</code> returns true, the process
* of registering the listener is immediately interrupted. <code>null</code>
* value disables this kind of callback.
* @param recurseInto a file filter that may return <code>false</code> when
* a folder should not be traversed into and external changes in it ignored.
* <code>null</code> recurses into all subfolders
* @since 7.61
*/
public static void addRecursiveListener(FileChangeListener listener, File path, FileFilter recurseInto, Callable<Boolean> stop) {
FileChangeImpl.addRecursiveListener(listener, path, recurseInto, stop);
}
/**
* Removes a listener to changes under given path.
* @param listener FileChangeListener to be removed
* @param path File path in which listener was listening
* @throws IllegalArgumentException if listener was not listening to given path
*
* @see FileObject#removeRecursiveListener
* @since org.openide.filesystems 7.28
*/
public static void removeRecursiveListener(FileChangeListener listener, File path) {
FileChangeImpl.removeRecursiveListener(listener, path);
}
/**
* Executes atomic action. For more info see {@link FileSystem#runAtomicAction}.
* <p>
* All events about filesystem changes (related to events on all affected instances of <code>FileSystem</code>)
* are postponed after the whole <code>atomicCode</code>
* is executed.
* </p>
* @param atomicCode code that is supposed to be run as atomic action. See {@link FileSystem#runAtomicAction}
* @throws java.io.IOException
* @since 7.5
*/
@SuppressWarnings("deprecation")
public static final void runAtomicAction(final AtomicAction atomicCode) throws IOException {
Repository.getDefault().getDefaultFileSystem().runAtomicAction(atomicCode);
}
/**
* Executes atomic action. For more info see {@link FileSystem#runAtomicAction}.
* <p>
* All events about filesystem changes (related to events on all affected instances of <code>FileSystem</code>)
* are postponed after the whole <code>atomicCode</code>
* is executed.
* </p>
* @param atomicCode code that is supposed to be run as atomic action. See {@link FileSystem#runAtomicAction}
* @since 7.5
*/
public static final void runAtomicAction(final Runnable atomicCode) {
final AtomicAction action = new FileSystem.AtomicAction() {
public void run() throws IOException {
atomicCode.run();
}
};
try {
FileUtil.runAtomicAction(action);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
/**
* Returns FileObject for a folder.
* If such a folder does not exist then it is created, including any necessary but nonexistent parent
* folders. Note that if this operation fails it may have succeeded in creating some of the necessary
* parent folders.
* @param folder folder to be created
* @return FileObject for a folder
* @throws java.io.IOException if the creation fails
* @since 7.0
*/
public static FileObject createFolder (final File folder) throws IOException {
File existingFolder = folder;
while(existingFolder != null && !existingFolder.isDirectory()) {
existingFolder = existingFolder.getParentFile();
}
if (existingFolder == null) {
throw new IOException(folder.getAbsolutePath());
}
FileObject retval = null;
FileObject folderFo = FileUtil.toFileObject(existingFolder);
assert folderFo != null : existingFolder.getAbsolutePath();
final String relativePath = getRelativePath(existingFolder, folder);
try {
retval = FileUtil.createFolder(folderFo,relativePath);
} catch (IOException ex) {
//thus retval = null;
}
//if refresh needed because of external changes
if (retval == null || !retval.isValid()) {
folderFo.getFileSystem().refresh(false);
retval = FileUtil.createFolder(folderFo,relativePath);
}
assert retval != null;
return retval;
}
/**Returns FileObject for a data file.
* If such a data file does not exist then it is created, including any necessary but nonexistent parent
* folders. Note that if this operation fails it may have succeeded in creating some of the necessary
* parent folders.
* @param data data file to be created
* @return FileObject for a data file
* @throws java.io.IOException if the creation fails
* @since 7.0
*/
public static FileObject createData (final File data) throws IOException {
File folder = data;
while(folder != null && !folder.isDirectory()) {
folder = folder.getParentFile();
}
if (folder == null) {
throw new IOException(data.getAbsolutePath());
}
FileObject retval = null;
FileObject folderFo = FileUtil.toFileObject(folder);
assert folderFo != null : folder.getAbsolutePath();
final String relativePath = getRelativePath(folder, data);
try {
retval = FileUtil.createData(folderFo,relativePath);
} catch (IOException ex) {
//thus retval = null;
}
//if refresh needed because of external changes
if (retval == null || !retval.isValid()) {
folderFo.getFileSystem().refresh(false);
retval = FileUtil.createData(folderFo,relativePath);
}
assert retval != null;
return retval;
}
private static String getRelativePath(final File dir, final File file) {
Stack<String> stack = new Stack<String>();
File tempFile = file;
while(tempFile != null && !tempFile.equals(dir)) {
stack.push (tempFile.getName());
tempFile = tempFile.getParentFile();
}
assert tempFile != null : file.getAbsolutePath() + "not found in " + dir.getAbsolutePath();//NOI18N
StringBuilder retval = new StringBuilder();
while (!stack.isEmpty()) {
retval.append(stack.pop());
if (!stack.isEmpty()) {
retval.append('/');//NOI18N
}
}
return retval.toString();
}
/** Copies stream of files.
* <P>
* Please be aware, that this method doesn't close any of passed streams.
* @param is input stream
* @param os output stream
*/
public static void copy(InputStream is, OutputStream os)
throws IOException {
final byte[] BUFFER = new byte[65536];
int len;
for (;;) {
len = is.read(BUFFER);
if (len == -1) {
return;
}
os.write(BUFFER, 0, len);
}
}
/** Copies file to the selected folder.
* This implementation simply copies the file by stream content.
* @param source source file object
* @param destFolder destination folder
* @param newName file name (without extension) of destination file
* @param newExt extension of destination file
* @return the created file object in the destination folder
* @exception IOException if <code>destFolder</code> is not a folder or does not exist; the destination file already exists; or
* another critical error occurs during copying
*/
static FileObject copyFileImpl(FileObject source, FileObject destFolder, String newName, String newExt)
throws IOException {
FileObject dest = destFolder.createData(newName, newExt);
FileLock lock = null;
InputStream bufIn = null;
OutputStream bufOut = null;
try {
lock = dest.lock();
bufIn = source.getInputStream();
if (dest instanceof AbstractFileObject) {
/** prevents from firing fileChange*/
bufOut = ((AbstractFileObject) dest).getOutputStream(lock, false);
} else {
bufOut = dest.getOutputStream(lock);
}
copy(bufIn, bufOut);
copyAttributes(source, dest);
} finally {
if (bufIn != null) {
bufIn.close();
}
if (bufOut != null) {
bufOut.close();
}
if (lock != null) {
lock.releaseLock();
}
}
return dest;
}
//
// public methods
//
/** Factory method that creates an empty implementation of a filesystem that
* completely resides in a memory.
* <p>To specify the MIME type of a data file without using a MIME resolver,
* set the {@code mimeType} file attribute.
* <p>Since 7.42, a {@link URLMapper} is available for files (and folders)
* in memory filesystems. These URLs are valid only so long as the filesystem
* has not been garbage-collected, so hold the filesystem (or a file in it)
* strongly for as long as you expect the URLs to be in play.
* @return a blank writable filesystem
* @since 4.43
*/
public static FileSystem createMemoryFileSystem() {
return new MemoryFileSystem();
}
/** Copies file to the selected folder.
* This implementation simply copies the file by stream content.
* @param source source file object
* @param destFolder destination folder
* @param newName file name (without extension) of destination file
* @param newExt extension of destination file
* @return the created file object in the destination folder
* @exception IOException if <code>destFolder</code> is not a folder or does not exist; the destination file already exists; or
* another critical error occurs during copying
*/
public static FileObject copyFile(FileObject source, FileObject destFolder, String newName, String newExt)
throws IOException {
return source.copy(destFolder, newName, newExt);
}
/** Copies file to the selected folder.
* This implementation simply copies the file by stream content.
* Uses the extension of the source file.
* @param source source file object
* @param destFolder destination folder
* @param newName file name (without extension) of destination file
* @return the created file object in the destination folder
* @exception IOException if <code>destFolder</code> is not a folder or does not exist; the destination file already exists; or
* another critical error occurs during copying
*/
public static FileObject copyFile(FileObject source, FileObject destFolder, String newName)
throws IOException {
return copyFile(source, destFolder, newName, source.getExt());
}
/** Moves file to the selected folder.
* This implementation uses a copy-and-delete mechanism, and automatically uses the necessary lock.
* @param source source file object
* @param destFolder destination folder
* @param newName file name (without extension) of destination file
* @return new file object
* @exception IOException if either the {@link #copyFile copy} or {@link FileObject#delete delete} failed
*/
public static FileObject moveFile(FileObject source, FileObject destFolder, String newName)
throws IOException {
FileLock lock = null;
try {
lock = source.lock();
return source.move(lock, destFolder, newName, source.getExt());
} finally {
if (lock != null) {
lock.releaseLock();
}
}
}
/** Returns a folder on given filesystem if such a folder exists.
* If not then a folder is created, including any necessary but nonexistent parent
* folders. Note that if this operation fails it may have succeeded in creating some of the necessary
* parent folders.
* The name of the new folder can be
* specified as a multi-component pathname whose components are separated
* by File.separatorChar or &quot;/&quot; (forward slash).
*
* @param folder where the new folder will be placed in
* @param name name of the new folder
* @return the new folder
* @exception IOException if the creation fails
*/
public static FileObject createFolder(FileObject folder, String name)
throws IOException {
String separators;
if (File.separatorChar != '/') {
separators = "/" + File.separatorChar; // NOI18N
} else {
separators = "/"; // NOI18N
}
StringTokenizer st = new StringTokenizer(name, separators);
if(name.startsWith("//") || name.startsWith("\\\\")) { // NOI18N
// if it is UNC absolute path, start with \\ComputerName\sharedFolder
try {
File root = new File("\\\\"+st.nextToken()+"\\"+st.nextToken()); // NOI18N
folder = FileUtil.toFileObject(root);
if (folder == null) {
throw new IOException("Windows share "+root.getPath()+" does not exist"); // NOI18N
}
} catch (NoSuchElementException ex) {
throw new IOException("Invalid Windows share "+name); // NOI18N
}
}
while (st.hasMoreElements()) {
name = st.nextToken();
if (name.length() > 0) {
FileObject f = folder.getFileObject(name);
if (f == null) {
try {
LOG.finest("createFolder - before create folder if not exists.");
f = folder.createFolder(name);
} catch (IOException ex) { // SyncFailedException or IOException when folder already exists
// there might be unconsistency between the cache
// and the disk, that is why
folder.refresh();
// and try again
f = folder.getFileObject(name);
if (f == null) {
// if still not found than we have to report the
// exception
throw ex;
}
}
}
folder = f;
}
}
return folder;
}
/** Returns a data file on given filesystem if such a data file exists.
* If not then a data file is created, including any necessary but nonexistent parent
* folders. Note that if this operation fails it may have succeeded in creating some of the necessary
* parent folders. The name of
* data file can be composed as resource name (e. g. org/netbeans/myfolder/mydata ).
*
* @param folder to begin with creation at
* @param name name of data file as a resource
* @return the data file for given name
* @exception IOException if the creation fails
*/
public static FileObject createData(FileObject folder, String name) throws IOException {
Parameters.notNull("folder", folder); //NOI18N
Parameters.notNull("name", name); //NOI18N
String foldername;
String dataname;
String fname;
String ext;
int index = name.lastIndexOf('/');
FileObject data;
// names with '/' on the end are not valid
if (index >= name.length()) {
throw new IOException("Wrong file name."); // NOI18N
}
// if name contains '/', create necessary folder first
if (index != -1) {
foldername = name.substring(0, index);
dataname = name.substring(index + 1);
folder = createFolder(folder, foldername);
assert folder != null;
} else {
dataname = name;
}
// create data
index = dataname.lastIndexOf('.');
if (index != -1) {
fname = dataname.substring(0, index);
ext = dataname.substring(index + 1);
} else {
fname = dataname;
ext = ""; // NOI18N
}
data = folder.getFileObject(fname, ext);
if (data == null) {
try {
data = folder.createData(fname, ext);
assert data != null : "FileObject.createData cannot return null; called on " + folder + " + " + fname +
" + " + ext; // #50802
} catch (SyncFailedException ex) {
// there might be unconsistency between the cache
// and the disk, that is why
folder.refresh();
// and try again
data = folder.getFileObject(fname, ext);
if (data == null) {
// if still not found than we have to report the
// exception
throw ex;
}
}
}
return data;
}
/** Finds appropriate java.io.File to FileObject if possible.
* If not possible then null is returned.
* This is the inverse operation of {@link #toFileObject}.
* @param fo FileObject whose corresponding File will be looked for
* @return java.io.File or null if no corresponding File exists.
* @since 1.29
*/
public static File toFile(FileObject fo) {
File retVal = (File) fo.getAttribute("java.io.File"); // NOI18N;
if (retVal == null) {
try {
if (fo.getFileSystem() instanceof JarFileSystem) {
return null;
}
} catch (FileStateInvalidException ex) {
return null;
}
URL fileURL = URLMapper.findURL(fo, URLMapper.INTERNAL);
if (fileURL == null || !"file".equals(fileURL.getProtocol())) { //NOI18N
fileURL = URLMapper.findURL(fo, URLMapper.EXTERNAL);
}
if ((fileURL != null) && "file".equals(fileURL.getProtocol())) {
retVal = BaseUtilities.toFile(URI.create(fileURL.toExternalForm()));
}
}
assert assertNormalized(retVal, BaseUtilities.isMac()); // #240180
return retVal;
}
/**
* Converts a disk file to a matching file object.
* This is the inverse operation of {@link #toFile}.
* <p class="nonnormative">
* If you are running with {@code org.netbeans.modules.masterfs} enabled,
* this method should never return null for a file which exists on disk.
* For example, to make this method work in unit tests in an Ant-based module project,
* right-click Unit Test Libraries, Add Unit Test Dependency, check Show Non-API Modules, select Master Filesystem.
* (Also right-click the new Master Filesystem node, Edit, uncheck Include in Compile Classpath.)
* To ensure masterfs (or some other module that can handle the conversion)
* is present put following line into your module manifest:
* </p>
* <pre>
* OpenIDE-Module-Needs: org.openide.filesystems.FileUtil.toFileObject
* </pre>
*
* @param file a disk file (may or may not exist). This file
* must be {@linkplain #normalizeFile normalized}.
* @return a corresponding file object, or null if the file does not exist
* or there is no {@link URLMapper} available to convert it
* @since 4.29
*/
public static FileObject toFileObject(File file) {
Parameters.notNull("file", file); //NOI18N
// return null for UNC root
if(file.getPath().equals("\\\\")) {
return null;
}
boolean asserts = false;
assert asserts = true;
if (asserts) {
File normFile = normalizeFile(file);
if (!file.equals(normFile)) {
final String msg = "Parameter file was not " + // NOI18N
"normalized. Was " + toDebugString(file) + " instead of " + toDebugString(normFile); // NOI18N
LOG.log(Level.WARNING, msg);
LOG.log(Level.INFO, msg, new IllegalArgumentException(msg));
}
file = normFile;
}
FileObject retVal = null;
try {
URL url = BaseUtilities.toURI(file).toURL();
retVal = URLMapper.findFileObject(url);
/*probably temporary piece of code to catch the cause of #46630*/
} catch (MalformedURLException e) {
retVal = null;
}
if (retVal != null) {
if (getDiskFileSystem() == null) {
try {
FileSystem fs = retVal.getFileSystem();
setDiskFileSystem(fs);
} catch (FileStateInvalidException ex) {
Exceptions.printStackTrace(ex);
}
}
}
return retVal;
}
/** Finds appropriate FileObjects to java.io.File if possible.
* If not possible then empty array is returned. More FileObjects may
* correspond to one java.io.File that`s why array is returned.
* @param file File whose corresponding FileObjects will be looked for.
* The file has to be "normalized" otherwise IllegalArgumentException is thrown.
* See {@link #normalizeFile} for how to do that.
* @return corresponding FileObjects or empty array if no
* corresponding FileObject exists.
* @since 1.29
* @deprecated Use {@link #toFileObject} instead.
*/
@Deprecated
public static FileObject[] fromFile(File file) {
FileObject[] retVal;
if (!file.equals(normalizeFile(file))) {
throw new IllegalArgumentException(
"Parameter file was not " + // NOI18N
"normalized. Was " + toDebugString(file) + " instead of " + toDebugString(normalizeFile(file))); // NOI18N
}
try {
URL url = (BaseUtilities.toURI(file).toURL());
retVal = URLMapper.findFileObjects(url);
} catch (MalformedURLException e) {
retVal = null;
}
return retVal;
}
/** Copies attributes from one file to another.
* Note: several special attributes will not be copied, as they should
* semantically be transient. These include attributes used by the
* template wizard (but not the template attribute itself).
* @param source source file object
* @param dest destination file object
* @exception IOException if the copying failed
*/
public static void copyAttributes(FileObject source, FileObject dest)
throws IOException {
Enumeration<String> attrKeys = source.getAttributes();
while (attrKeys.hasMoreElements()) {
String key = attrKeys.nextElement();
if (transientAttributes.contains(key)) {
continue;
}
if (isTransient(source, key)) {
continue;
}
AtomicBoolean isRawValue = new AtomicBoolean();
Object value = XMLMapAttr.getRawAttribute(source, key, isRawValue);
// #132801 and #16761 - don't set attributes where value is
// instance of VoidValue because these attributes were previously written
// by mistake in code. So it should happen only if you import some
// settings from old version.
if (value != null && !(value instanceof MultiFileObject.VoidValue)) {
if (isRawValue.get() && value instanceof Method) {
dest.setAttribute("methodvalue:" + key, value); // NOI18N
} else if (isRawValue.get() && value instanceof Class) {
dest.setAttribute("newvalue:" + key, value); // NOI18N
} else {
dest.setAttribute(key, value);
}
}
}
}
static boolean isTransient(FileObject fo, String attrName) {
return XMLMapAttr.ModifiedAttribute.isTransient(fo, attrName);
}
/** Extract jar file into folder represented by file object. If the JAR contains
* files with name filesystem.attributes, it is assumed that these files
* has been created by DefaultAttributes implementation and the content
* of these files is treated as attributes and added to extracted files.
* <p><code>META-INF/</code> directories are skipped over.
*
* @param fo file object of destination folder
* @param is input stream of jar file
* @exception IOException if the extraction fails
* @deprecated Use of XML filesystem layers generally obsoletes this method.
* For tests, use {@link org.openide.util.test.TestFileUtils#unpackZipFile}.
*/
@Deprecated
public static void extractJar(final FileObject fo, final InputStream is)
throws IOException {
FileSystem fs = fo.getFileSystem();
fs.runAtomicAction(
new FileSystem.AtomicAction() {
public void run() throws IOException {
extractJarImpl(fo, is);
}
}
);
}
/** Does the actual extraction of the Jar file.
*/
private static void extractJarImpl(FileObject fo, InputStream is)
throws IOException {
JarInputStream jis;
JarEntry je;
// files with extended attributes (name, DefaultAttributes.Table)
HashMap<String, DefaultAttributes.Table> attributes =
new HashMap<String, DefaultAttributes.Table>(7);
jis = new JarInputStream(is);
while ((je = jis.getNextJarEntry()) != null) {
String name = je.getName();
if (name.toLowerCase().startsWith("meta-inf/")) {
continue; // NOI18N
}
if (je.isDirectory()) {
createFolder(fo, name);
continue;
}
if (DefaultAttributes.acceptName(name)) {
// file with extended attributes
DefaultAttributes.Table table = DefaultAttributes.loadTable(jis, name);
attributes.put(name, table);
} else {
// copy the file
FileObject fd = createData(fo, name);
FileLock lock = fd.lock();
try {
OutputStream os = fd.getOutputStream(lock);
try {
copy(jis, os);
} finally {
os.close();
}
} finally {
lock.releaseLock();
}
}
}
//
// apply all extended attributes
//
for (Map.Entry entry : attributes.entrySet()) {
String fileName = (String) entry.getKey();
int last = fileName.lastIndexOf('/');
String dirName;
if (last != -1) {
dirName = fileName.substring(0, last + 1);
} else {
dirName = ""; // NOI18N
}
String prefix = fo.isRoot() ? dirName : (fo.getPath() + '/' + dirName);
DefaultAttributes.Table t = (DefaultAttributes.Table) entry.getValue();
Iterator files = t.keySet().iterator();
while (files.hasNext()) {
String orig = (String) files.next();
String fn = prefix + orig;
FileObject obj = fo.getFileSystem().findResource(fn);
if (obj == null) {
continue;
}
Enumeration<String> attrEnum = t.attrs(orig);
while (attrEnum.hasMoreElements()) {
// iterate thru all arguments
String attrName = attrEnum.nextElement();
// Note: even transient attributes set here!
Object value = t.getAttr(orig, attrName);
if (value != null) {
obj.setAttribute(attrName, value);
}
}
}
}
}
// extractJar
/** Gets the extension of a specified file name. The extension is
* everything after the last dot.
*
* @param fileName name of the file
* @return extension of the file (or <code>""</code> if it had none)
*/
public static String getExtension(String fileName) {
int index = fileName.lastIndexOf("."); // NOI18N
if (index == -1) {
return ""; // NOI18N
} else {
return fileName.substring(index + 1);
}
}
/** Finds an unused file name similar to that requested in the same folder.
* The specified file name is used if that does not yet exist or is
* {@link FileObject#isVirtual isVirtual}.
* Otherwise, the first available name of the form <code>basename_nnn.ext</code> (counting from one) is used.
*
* <p><em>Caution:</em> this method does not lock the parent folder
* to prevent race conditions: i.e. it is possible (though unlikely)
* that the resulting name will have been created by another thread
* just as you were about to create the file yourself (if you are,
* in fact, intending to create it just after this call). Since you
* cannot currently lock a folder against child creation actions,
* the safe approach is to use a loop in which a free name is
* retrieved; an attempt is made to {@link FileObject#createData create}
* that file; and upon an <code>IOException</code> during
* creation, retry the loop up to a few times before giving up.
*
* @param folder parent folder
* @param name preferred base name of file
* @param ext extension to use (or null)
* @return a free file name <strong>(without the extension)</strong>
*/
public static String findFreeFileName(FileObject folder, String name, String ext) {
if (checkFreeName(folder, name, ext)) {
return name;
}
for (int i = 1;; i++) {
String destName = name + "_" + i; // NOI18N
if (checkFreeName(folder, destName, ext)) {
return destName;
}
}
}
/** Finds an unused folder name similar to that requested in the same parent folder.
* <p>See caveat for <code>findFreeFileName</code>.
* @see #findFreeFileName findFreeFileName
* @param folder parent folder
* @param name preferred folder name
* @return a free folder name
*/
public static String findFreeFolderName(FileObject folder, String name) {
if (checkFreeName(folder, name, null)) {
return name;
}
for (int i = 1;; i++) {
String destName = name + "_" + i; // NOI18N
if (checkFreeName(folder, destName, null)) {
return destName;
}
}
}
/**
* Gets a relative resource path between folder and fo.
* @param folder root of filesystem or any other folder in folders hierarchy
* @param fo arbitrary FileObject in folder's tree (including folder itself)
* @return relative path between folder and fo. The returned path never
* starts with a '/'. It never ends with a '/'. Specifically, if
* folder==fo, returns "". Returns <code>null</code> if fo is not in
* folder's tree.
* @see #isParentOf
* @since 4.16
*/
public static String getRelativePath(FileObject folder, FileObject fo) {
if (!isParentOf(folder, fo) && (folder != fo)) {
return null;
}
String result = fo.getPath().substring(folder.getPath().length());
if (result.startsWith("/") && !result.startsWith("//")) {
result = result.substring(1);
}
return result;
}
/** Test if given name is free in given folder.
* @param fo folder to check in
* @param name name of the file or folder to check
* @param ext extension of the file (null for folders)
* @return true, if such name does not exists
*/
private static boolean checkFreeName(FileObject fo, String name, String ext) {
if ((BaseUtilities.isWindows() || (BaseUtilities.getOperatingSystem() == BaseUtilities.OS_OS2)) || BaseUtilities.isMac()) {
// case-insensitive, do some special check
Enumeration<? extends FileObject> en = fo.getChildren(false);
while (en.hasMoreElements()) {
fo = en.nextElement();
String n = fo.getName();
String e = fo.getExt();
// different names => check others
if (!n.equalsIgnoreCase(name)) {
continue;
}
// same name + without extension => no
if (((ext == null) || (ext.trim().length() == 0)) && ((e == null) || (e.trim().length() == 0))) {
return fo.isVirtual();
}
// one of there is witout extension => check next
if ((ext == null) || (e == null)) {
continue;
}
if (ext.equalsIgnoreCase(e)) {
// same name + same extension => no
return fo.isVirtual();
}
}
// no of the files has similar name and extension
return true;
} else {
if (ext == null) {
fo = fo.getFileObject(name);
if (fo == null) {
return true;
}
return fo.isVirtual();
} else {
fo = fo.getFileObject(name, ext);
if (fo == null) {
return true;
}
return fo.isVirtual();
}
}
}
// note: "sister" is preferred in English, please don't ask me why --jglick // NOI18N
/** Finds brother file with same base name but different extension.
* @param fo the file to find the brother for or <CODE>null</CODE>
* @param ext extension for the brother file
* @return a brother file (with the requested extension and the same parent folder as the original) or
* <CODE>null</CODE> if the brother file does not exist or the original file was <CODE>null</CODE>
*/
public static FileObject findBrother(FileObject fo, String ext) {
if (fo == null) {
return null;
}
FileObject parent = fo.getParent();
if (parent == null) {
return null;
}
return parent.getFileObject(fo.getName(), ext);
}
/** Obtain MIME type for a well-known extension.
* If there is a case-sensitive match, that is used, else will fall back
* to a case-insensitive match.
* @param ext the extension: <code>"jar"</code>, <code>"zip"</code>, etc.
* @return the MIME type for the extension, or <code>null</code> if the extension is unrecognized
* @deprecated use {@link #getMIMEType(FileObject)} or {@link #getMIMEType(FileObject, String[])}
* as MIME cannot be generally detected by file object extension.
*/
@Deprecated
public static String getMIMEType(String ext) {
assert false : "FileUtil.getMIMEType(String extension) is deprecated. Please, use FileUtil.getMIMEType(FileObject)."; //NOI18N
if (ext.toLowerCase().equals("xml")) { //NOI18N
return "text/xml"; // NOI18N
}
return null;
}
/** Resolves MIME type. Registered resolvers are invoked and used to achieve this goal.
* Resolvers must subclass MIMEResolver.
* @param fo whose MIME type should be recognized
* @return the MIME type for the FileObject, or {@code null} if the FileObject is unrecognized.
* It may return {@code content/unknown} instead of {@code null}.
*/
public static String getMIMEType(FileObject fo) {
return MIMESupport.findMIMEType(fo);
}
/** Resolves MIME type. Registered resolvers are invoked and used to achieve this goal.
* Resolvers must subclass MIMEResolver. By default it is possible for the
* method to return {@code content/unknown} instead of {@code null} - if
* you want to avoid such behavior, include <code>null</code> in the
* list of requested <code>withinMIMETypes</code> - in such case the
* return value is guaranteed to be one of the values in <code>withinMIMETypes</code>
* or <code>null</code>.
* <p>
* Example: Check if some file is Java source file or text file:
* </p>
* <code>
* FileUtil.getMIMEType(fo, null, "text/x-java", "text/plain") != null
* </code>
* @param fo whose MIME type should be recognized
* @param withinMIMETypes an array of MIME types. Only resolvers whose
* {@link MIMEResolver#getMIMETypes} contain one or more of the requested
* MIME types will be asked if they recognize the file. It is possible for
* the resulting MIME type to not be a member of this list.
* @return the MIME type for the FileObject, or <code>null</code> if
* the FileObject is unrecognized.
* @since 7.13
*/
public static String getMIMEType(FileObject fo, String... withinMIMETypes) {
Parameters.notNull("withinMIMETypes", withinMIMETypes); //NOI18N
String res = MIMESupport.findMIMEType(fo, withinMIMETypes);
if (res == null) {
return null;
}
boolean foundNull = false;
for(String t : withinMIMETypes) {
if (t == null) {
foundNull = true;
continue;
}
if (res.equals(t)) {
return t;
}
}
return foundNull ? null : res;
}
/** Registers specified extension to be recognized as specified MIME type.
* If MIME type parameter is null, it cancels previous registration.
* Note that you may register a case-sensitive extension if that is
* relevant (for example {@literal *.C} for C++) but if you register
* a lowercase extension it will by default apply to uppercase extensions
* too on Windows.
* @param extension the file extension to be registered
* @param mimeType the MIME type to be registered for the extension or {@code null} to deregister
* @see #getMIMEType(FileObject)
* @see #getMIMETypeExtensions(String)
*/
public static void setMIMEType(String extension, String mimeType) {
Parameters.notEmpty("extension", extension); //NOI18N
final Map<String, Set<String>> mimeToExtensions = new HashMap<String, Set<String>>();
FileObject userDefinedResolverFO = MIMEResolverImpl.getUserDefinedResolver();
if (userDefinedResolverFO != null) {
// add all previous content
mimeToExtensions.putAll(MIMEResolverImpl.getMIMEToExtensions(userDefinedResolverFO));
// exclude extension possibly registered for other MIME types
for (Set<String> extensions : mimeToExtensions.values()) {
extensions.remove(extension);
}
}
if (mimeType != null) {
// add specified extension to our structure
Set<String> previousExtensions = mimeToExtensions.get(mimeType);
if (previousExtensions != null) {
previousExtensions.add(extension);
} else {
mimeToExtensions.put(mimeType, Collections.singleton(extension));
}
}
if (MIMEResolverImpl.storeUserDefinedResolver(mimeToExtensions)) {
MIMESupport.resetCache();
}
MIMESupport.freeCaches();
}
/** Returns list of file extensions associated with specified MIME type. In
* other words files with those extensions are recognized as specified MIME type
* in NetBeans' filesystem. It never returns {@code null}.
* @param mimeType the MIME type (e.g. image/gif)
* @return list of file extensions associated with specified MIME type, never {@code null}
* @see #setMIMEType(String, String)
* @since org.openide.filesystems 7.18
*/
public static List<String> getMIMETypeExtensions(String mimeType) {
Parameters.notEmpty("mimeType", mimeType); //NOI18N
HashMap<String, String> extensionToMime = new HashMap<String, String>();
for (FileObject mimeResolverFO : MIMEResolverImpl.getOrderedResolvers()) {
Map<String, Set<String>> mimeToExtensions = MIMEResolverImpl.getMIMEToExtensions(mimeResolverFO);
for (Map.Entry<String, Set<String>> entry : mimeToExtensions.entrySet()) {
String mimeKey = entry.getKey();
Set<String> extensions = entry.getValue();
for (String extension : extensions) {
extensionToMime.put(extension, mimeKey);
}
}
}
List<String> registeredExtensions = new ArrayList<String>();
for (Map.Entry<String, String> entry : extensionToMime.entrySet()) {
if (entry.getValue().equals(mimeType)) {
registeredExtensions.add(entry.getKey());
}
}
return registeredExtensions;
}
/**
* @deprecated No longer used.
*/
@Deprecated
public static URLStreamHandler nbfsURLStreamHandler() {
return new FileURL.Handler();
}
/** Recursively checks whether the file is underneath the folder. It checks whether
* the file and folder are located on the same filesystem, in such case it checks the
* parent <code>FileObject</code> of the file recursively until the folder is found
* or the root of the filesystem is reached.
* <p><strong>Warning:</strong> this method will return false in the case that
* <code>folder == fo</code>.
* @param folder the root of folders hierarchy to search in
* @param fo the file to search for
* @return <code>true</code>, if <code>fo</code> lies somewhere underneath the <code>folder</code>,
* <code>false</code> otherwise
* @since 3.16
*/
public static boolean isParentOf(FileObject folder, FileObject fo) {
Parameters.notNull("folder", folder); //NOI18N
Parameters.notNull("fo", fo); //NOI18N
if (folder.isData()) {
return false;
}
try {
if (folder.getFileSystem() != fo.getFileSystem()) {
return false;
}
} catch (FileStateInvalidException e) {
return false;
}
FileObject parent = fo.getParent();
while (parent != null) {
if (parent.equals(folder)) {
return true;
}
parent = parent.getParent();
}
return false;
}
/**
* Check whether some FileObject is a recursive symbolic link.
*
* @param fo FileObject to check.
*
* @return True if the FileObject represents a symbolic link referring to a
* directory that is parent of this FileObject, false otherwise.
* @throws java.io.IOException If some I/O problem occurs.
* @since openide.filesystem/9.4
*/
public static boolean isRecursiveSymbolicLink(FileObject fo)
throws IOException {
if (!fo.isFolder()) {
return false;
} else if (fo.isSymbolicLink()) {
FileObject parent = fo.getParent();
if (parent == null) {
return false;
}
FileObject target = fo.readSymbolicLink();
if (target != null) {
FileObject realParent = parent.getCanonicalFileObject();
FileObject realTarget = target.getCanonicalFileObject();
return realParent != null && realTarget != null
&& (realTarget.equals(realParent)
|| FileUtil.isParentOf(realTarget, realParent));
} else {
return false;
}
} else {
return false;
}
}
/** Creates a weak implementation of FileChangeListener.
*
* @param l the listener to delegate to
* @param source the source that the listener should detach from when
* listener <CODE>l</CODE> is freed, can be <CODE>null</CODE>
* @return a FileChangeListener delegating to <CODE>l</CODE>.
* @since 4.10
*/
public static FileChangeListener weakFileChangeListener(FileChangeListener l, Object source) {
return WeakListeners.create(FileChangeListener.class, l, source);
}
/** Creates a weak implementation of FileStatusListener.
*
* @param l the listener to delegate to
* @param source the source that the listener should detach from when
* listener <CODE>l</CODE> is freed, can be <CODE>null</CODE>
* @return a FileChangeListener delegating to <CODE>l</CODE>.
* @since 4.10
*/
public static FileStatusListener weakFileStatusListener(FileStatusListener l, Object source) {
return WeakListeners.create(FileStatusListener.class, l, source);
}
/**
* Get an appropriate display name for a file object.
* If the file corresponds to a path on disk, this will be the disk path.
* Otherwise the name will mention the filesystem name or archive name in case
* the file comes from archive and relative path. Relative path will be mentioned
* just in case that passed <code>FileObject</code> isn't root {@link FileObject#isRoot}.
*
* @param fo a file object
* @return a display name indicating where the file is
* @since 4.39
*/
public static String getFileDisplayName(FileObject fo) {
String displayName = null;
File f = FileUtil.toFile(fo);
if (f != null) {
displayName = f.getAbsolutePath();
} else {
FileObject archiveFile = FileUtil.getArchiveFile(fo);
if (archiveFile != null) {
displayName = getArchiveDisplayName(fo, archiveFile);
}
}
if (displayName == null) {
try {
if (fo.isRoot()) {
displayName = fo.getFileSystem().getDisplayName();
} else {
displayName = NbBundle.getMessage(
FileUtil.class, "LBL_file_in_filesystem", fo.getPath(), fo.getFileSystem().getDisplayName()
);
}
} catch (FileStateInvalidException e) {
// Not relevant now, just use the simple path.
displayName = fo.getPath();
}
}
return displayName;
}
private static String getArchiveDisplayName(FileObject fo, FileObject archiveFile) {
String displayName = null;
File f = FileUtil.toFile(archiveFile);
if (f != null) {
String archivDisplayName = f.getAbsolutePath();
if (fo.isRoot()) {
displayName = archivDisplayName;
} else {
String entryPath = fo.getPath();
displayName = NbBundle.getMessage(
FileUtil.class, "LBL_file_in_filesystem", entryPath, archivDisplayName
);
}
}
return displayName;
}
/**
* See {@link #normalizeFile} for details
* @param path file path to normalize
* @return normalized file path
* @since 7.42
*/
public static String normalizePath(final String path) {
Map<String, String> normalizedPaths = getNormalizedFilesMap();
String normalized = normalizedPaths.get(path);
if (normalized == null) {
File ret = normalizeFileImpl(new File(path));
normalized = ret.getPath();
normalizedPaths.put(path, normalized);
}
return normalized;
}
/**
* Normalize a file path to a clean form.
* This method may for example make sure that the returned file uses
* the natural case on Windows; that old Windows 8.3 filenames are changed to the long form;
* that relative paths are changed to be
* absolute; that <code>.</code> and <code>..</code> sequences are removed; etc.
* Unlike {@link File#getCanonicalFile} this method will not traverse symbolic links on Unix.
* <p>This method involves some overhead and should not be called frivolously.
* Generally it should be called on <em>incoming</em> pathnames that are gotten from user input
* (including filechoosers), configuration files, Ant properties, etc. <em>Internal</em>
* calculations should not need to renormalize paths since {@link File#listFiles},
* {@link File#getParentFile}, etc. will not produce abnormal variants.
* @param file file to normalize
* @return normalized file
* @since 4.48
*/
public static File normalizeFile(final File file) {
File ret = normalizeFileCached(file);
assert assertNormalized(ret);
return ret;
}
private static File normalizeFileCached(final File file) {
Map<String, String> normalizedPaths = getNormalizedFilesMap();
String unnormalized = file.getPath();
String normalized = normalizedPaths.get(unnormalized);
if (
normalized != null &&
normalized.equalsIgnoreCase(unnormalized) &&
!normalized.equals(unnormalized)
) {
normalized = null;
}
File ret;
if (normalized == null) {
ret = normalizeFileImpl(file);
assert !ret.getName().equals(".") : "Original file " + file + " normalized: " + ret;
normalizedPaths.put(unnormalized, ret.getPath());
} else if (normalized.equals(unnormalized)) {
ret = file;
} else {
ret = new File(normalized);
}
return ret;
}
private static File normalizeFileImpl(File file) {
// XXX should use NIO in JDK 7; see #6358641
Parameters.notNull("file", file); //NOI18N
File retFile;
LOG.log(Level.FINE, "FileUtil.normalizeFile for {0}", file); // NOI18N
long now = System.currentTimeMillis();
if ((BaseUtilities.isWindows() || (BaseUtilities.getOperatingSystem() == BaseUtilities.OS_OS2))) {
retFile = normalizeFileOnWindows(file);
} else if (BaseUtilities.isMac()) {
retFile = normalizeFileOnMac(file);
} else {
retFile = normalizeFileOnUnixAlike(file);
}
File ret = (file.getPath().equals(retFile.getPath())) ? file : retFile;
long took = System.currentTimeMillis() - now;
if (took > 500) {
LOG.log(Level.WARNING, "FileUtil.normalizeFile({0}) took {1} ms. Result is {2}", new Object[]{file, took, ret});
}
return ret;
}
private static File normalizeFileOnUnixAlike(File file) {
// On Unix, do not want to traverse symlinks.
// URI.normalize removes ../ and ./ sequences nicely.
file = BaseUtilities.toFile(BaseUtilities.toURI(file).normalize()).getAbsoluteFile();
while (file.getAbsolutePath().startsWith("/../")) { // NOI18N
file = new File(file.getAbsolutePath().substring(3));
}
if (file.getAbsolutePath().equals("/..")) { // NOI18N
// Special treatment.
file = new File("/"); // NOI18N
}
return file;
}
private static File normalizeFileOnMac(final File file) {
File retVal = file;
File absoluteFile = BaseUtilities.toFile(BaseUtilities.toURI(file).normalize());
String absolutePath = absoluteFile.getAbsolutePath();
if (absolutePath.equals("/..")) { // NOI18N
// Special treatment.
absoluteFile = new File(absolutePath = "/"); // NOI18N
}
try {
// URI.normalize removes ../ and ./ sequences nicely.
File canonicalFile = file.getCanonicalFile();
boolean isSymLink = !canonicalFile.getAbsolutePath().equalsIgnoreCase(absolutePath);
if (isSymLink) {
retVal = normalizeSymLinkOnMac(absoluteFile);
} else {
retVal = canonicalFile;
}
} catch (IOException ioe) {
LOG.log(Level.FINE, "Normalization failed on file " + file, ioe);
// OK, so at least try to absolutize the path
retVal = absoluteFile.getAbsoluteFile();
}
return retVal;
}
/**
* @param file is expected to be already absolute with removed ../ and ./
*/
private static File normalizeSymLinkOnMac(final File file)
throws IOException {
File retVal = File.listRoots()[0];
File pureCanonicalFile = retVal;
final String pattern = File.separator + ".." + File.separator; //NOI18N
final String fileName;
{ // strips insufficient non-<tt>".."</tt> segments preceding them
String tmpFileName = file.getAbsolutePath();
int index = tmpFileName.lastIndexOf(pattern);
if (index > -1) {
tmpFileName = tmpFileName.substring(index + pattern.length()); //Remove starting {/../}*
}
fileName = tmpFileName;
}
/*normalized step after step*/
StringTokenizer fileSegments = new StringTokenizer(fileName, File.separator);
while (fileSegments.hasMoreTokens()) {
File absolutelyEndingFile = new File(pureCanonicalFile, fileSegments.nextToken());
pureCanonicalFile = absolutelyEndingFile.getCanonicalFile();
boolean isSymLink = !pureCanonicalFile.getAbsolutePath().equalsIgnoreCase(
absolutelyEndingFile.getAbsolutePath()
);
if (isSymLink) {
retVal = new File(retVal, absolutelyEndingFile.getName());
} else {
retVal = new File(retVal, pureCanonicalFile.getName());
}
}
return retVal;
}
private static File normalizeFileOnWindows(final File file) {
File retVal = null;
if (file.getClass().getName().startsWith("sun.awt.shell")) { // NOI18N
return file;
}
try {
retVal = file.getCanonicalFile();
if (retVal.getName().equals(".")) { // NOI18Ny
// try one more time
retVal = retVal.getCanonicalFile();
}
} catch (IOException e) {
String path = file.getPath();
// report only other than UNC path \\ or \\computerName because these cannot be canonicalized
if (!path.equals("\\\\") && !("\\\\".equals(file.getParent()))) { //NOI18N
LOG.log(Level.FINE, path, e);
}
if (path.endsWith(".")) {
path = path.substring(0, path.length() - 1);
retVal = new File(path);
}
if (path.length() > 3 && path.endsWith("\\")) {
path = path.substring(0, path.length() - 1);
retVal = new File(path);
}
}
// #135547 - on Windows Vista map "Documents and Settings\<username>\My Documents" to "Users\<username>\Documents"
if((BaseUtilities.getOperatingSystem() & BaseUtilities.OS_WINVISTA) != 0) {
if(retVal == null) {
retVal = file.getAbsoluteFile();
}
String absolutePath = retVal.getAbsolutePath();
if(absolutePath.contains(":\\Documents and Settings")) { //NOI18N
absolutePath = absolutePath.replaceFirst("Documents and Settings", "Users"); //NOI18N
absolutePath = absolutePath.replaceFirst("My Documents", "Documents"); //NOI18N
absolutePath = absolutePath.replaceFirst("My Pictures", "Pictures"); //NOI18N
absolutePath = absolutePath.replaceFirst("My Music", "Music"); //NOI18N
retVal = new File(absolutePath);
}
}
return (retVal != null) ? retVal : file.getAbsoluteFile();
}
private static Reference<Map<String, String>> normalizedRef = new SoftReference<Map<String, String>>(new ConcurrentHashMap<String, String>());
private static Map<String, String> getNormalizedFilesMap() {
Map<String, String> map = normalizedRef.get();
if (map == null) {
synchronized (FileUtil.class) {
map = normalizedRef.get();
if (map == null) {
map = new ConcurrentHashMap<String, String>();
normalizedRef = new SoftReference<Map<String, String>>(map);
}
}
}
return map;
}
static void freeCaches() {
normalizedRef.clear();
}
/**
* Returns a FileObject representing the root folder of an archive.
* Clients may need to first call {@link #isArchiveFile(FileObject)} to determine
* if the file object refers to an archive file.
* @param fo a java archive file, by default ZIP and JAR are supported
* @return a virtual archive root folder, or null if the file is not actually an archive
* @since 4.48
*/
public static FileObject getArchiveRoot(FileObject fo) {
for (ArchiveRootProvider provider : getArchiveRootProviders()) {
if (provider.isArchiveFile(fo, false)) {
final FileObject root = provider.getArchiveRoot(fo);
if (root != null) {
return root;
}
}
}
return null;
}
/**
* Returns a URL representing the root of an archive.
* Clients may need to first call {@link #isArchiveFile(URL)} to determine if the URL
* refers to an archive file.
* @param url of a java archive file, by default ZIP and JAR are supported
* @return the archive (eg. <code>jar</code>) protocol URL of the root of the archive.
* @since 4.48
*/
public static URL getArchiveRoot(URL url) {
for (ArchiveRootProvider provider : getArchiveRootProviders()) {
if (provider.isArchiveFile(url, false)) {
final URL root = provider.getArchiveRoot(url);
if (root != null) {
return root;
}
}
}
//For compatibility reason never return null but return the jar URL.
return getArchiveRootProviders().iterator().next().getArchiveRoot(url);
}
/**
* Returns a FileObject representing an archive file containing the
* FileObject given by the parameter.
* <strong>Remember</strong> that any path within the archive is discarded
* so you may need to check for non-root entries.
* @param fo a file in an archive filesystem
* @return the file corresponding to the archive itself,
* or null if <code>fo</code> is not an archive entry
* @since 4.48
*/
public static FileObject getArchiveFile(FileObject fo) {
Parameters.notNull("fo", fo); //NOI18N
for (ArchiveRootProvider provider : getArchiveRootProviders()) {
if (provider.isArchiveArtifact(fo)) {
final FileObject file = provider.getArchiveFile(fo);
if (file != null) {
return file;
}
}
}
return null;
}
/**
* Returns the URL of the archive file containing the file
* referred to by an archive (eg. <code>jar</code>) protocol URL.
* <strong>Remember</strong> that any path within the archive is discarded
* so you may need to check for non-root entries.
* @param url a URL
* @return the embedded archive URL, or null if the URL is not an
* archive protocol URL containing <code>!/</code>
* @since 4.48
*/
public static URL getArchiveFile(URL url) {
for (ArchiveRootProvider provider : getArchiveRootProviders()) {
if (provider.isArchiveArtifact(url)) {
final URL file = provider.getArchiveFile(url);
if (file != null) {
return file;
}
}
}
return null;
}
/**
* Tests if a file represents a java archive.
* By default the JAR or ZIP archives are supported.
* @param fo the file to be tested
* @return true if the file looks like a java archive
* @since 4.48
*/
public static boolean isArchiveFile(FileObject fo) {
Parameters.notNull("fileObject", fo); //NOI18N
for (ArchiveRootProvider provider : getArchiveRootProviders()) {
if (provider.isArchiveFile(fo, true)) {
return true;
}
}
return false;
}
/**
* Tests if a URL represents a java archive.
* By default the JAR or ZIP archives are supported.
* If there is no such file object, the test is done by heuristic: any URL with an extension is
* treated as an archive.
* @param url a URL to a file
* @return true if the URL seems to represent a java archive
* @since 4.48
*/
public static boolean isArchiveFile(URL url) {
Parameters.notNull("url", url); //NOI18N
return isArchiveFileImpl(url, true);
}
/**
* Tests if an file is inside an archive.
* @param fo the file to be tested
* @return true if the file is inside an archive
* @since 9.10
*/
public static boolean isArchiveArtifact(FileObject fo) {
Parameters.notNull("fileObject", fo); //NOI18N
for (ArchiveRootProvider provider : getArchiveRootProviders()) {
if (provider.isArchiveArtifact(fo)) {
return true;
}
}
return false;
}
/**
* Tests if an {@link URL} denotes a file inside an archive.
* @param url the url to be tested
* @return true if the url points inside an archive
* @since 9.10
*/
public static boolean isArchiveArtifact(URL url) {
Parameters.notNull("url", url); //NOI18N
for (ArchiveRootProvider provider : getArchiveRootProviders()) {
if (provider.isArchiveArtifact(url)) {
return true;
}
}
return false;
}
/**
* Convert a file such as would be shown in a classpath entry into a proper folder URL.
* If the file looks to represent a directory, a <code>file</code> URL will be created.
* If it looks to represent a ZIP archive, a <code>jar</code> URL will be created.
* @param entry a file or directory name
* @return an appropriate classpath URL which will always end in a slash (<samp>/</samp>),
* or null for an existing file which does not look like a valid archive
* @since org.openide.filesystems 7.8
*/
public static URL urlForArchiveOrDir(File entry) {
try {
boolean wasDir;
boolean isDir;
URL u;
do {
wasDir = entry.isDirectory();
LOG.finest("urlForArchiveOrDir:toURI:entry"); //NOI18N
u = BaseUtilities.toURI(entry).toURL();
isDir = entry.isDirectory();
} while (wasDir ^ isDir);
if (isArchiveFileImpl(u, false)) {
return getArchiveRoot(u);
} else if (isDir) {
assert u.toExternalForm().endsWith("/"); //NOI18N
return u;
} else if (!entry.exists()) {
if (!u.toString().endsWith("/")) { //NOI18N
u = new URL(u + "/"); // NOI18N
}
return u;
} else {
return null;
}
} catch (MalformedURLException x) {
assert false : x;
return null;
}
}
/**
* Convert a classpath-type URL to a corresponding file.
* If it is a <code>jar</code> URL representing the root folder of a local disk archive,
* that archive file will be returned.
* If it is a <code>file</code> URL representing a local disk folder,
* that folder will be returned.
* @param entry a classpath entry or similar URL
* @return a corresponding file, or null for e.g. a network URL or non-root JAR folder entry
* @since org.openide.filesystems 7.8
*/
public static File archiveOrDirForURL(URL entry) {
final String u = entry.toString();
if (isArchiveArtifact(entry)) {
entry = getArchiveFile(entry);
try {
return u.endsWith("!/") && entry != null && "file".equals(entry.getProtocol()) ? //NOI18N
BaseUtilities.toFile(entry.toURI()):
null;
} catch (URISyntaxException e) {
LOG.log(
Level.WARNING,
"Invalid URI: {0} ({1})", //NOI18N
new Object[]{
entry,
e.getMessage()
});
return null;
}
} else if (u.startsWith("file:")) { // NOI18N
return BaseUtilities.toFile(URI.create(u));
} else {
return null;
}
}
/**
* Make sure that a JFileChooser does not traverse symlinks on Unix.
* @param chooser a file chooser
* @param currentDirectory if not null, a file to set as the current directory
* using {@link javax.swing.JFileChooser#setCurrentDirectory} without canonicalizing
* @see <a href="http://www.netbeans.org/issues/show_bug.cgi?id=46459">Issue #46459</a>
* @see <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4906607">JRE bug #4906607</a>
* @since org.openide/1 4.42
* @deprecated Just use {@link javax.swing.JFileChooser#setCurrentDirectory}. JDK 6 does not have this bug.
*/
@Deprecated
public static void preventFileChooserSymlinkTraversal(javax.swing.JFileChooser chooser, File currentDirectory) {
chooser.setCurrentDirectory(currentDirectory);
}
/**
* Sorts some sibling file objects.
* <p>Normally this is done by looking for numeric file attributes named <code>position</code>
* on the children; children with a lower position number are placed first.
* Now-deprecated relative ordering attributes of the form <code>earlier/later</code> may
* also be used; if the above attribute has a boolean value of <code>true</code>,
* then the file named <code>earlier</code> will be sorted somewhere (not necessarily directly)
* before the file named <code>later</code>. Numeric and relative attributes may also be mixed.</p>
* <p>The sort is <em>stable</em> at least to the extent that if there is no ordering information
* whatsoever, the returned list will be in the same order as the incoming collection.</p>
* @param children zero or more files (or folders); must all have the same {@link FileObject#getParent}
* @param logWarnings true to log warnings about relative ordering attributes or other semantic problems, false to keep quiet
* @return a sorted list of the same children
* @throws IllegalArgumentException in case there are duplicates, or nulls, or the files do not have a common parent
* @since org.openide.filesystems 7.2
* @see #setOrder
* @see <a href="http://wiki.netbeans.org/wiki/view/FolderOrdering103187">Specification</a>
*/
public static List<FileObject> getOrder(Collection<FileObject> children, boolean logWarnings) throws IllegalArgumentException {
return Ordering.getOrder(children, logWarnings);
}
/**
* Imposes an order on some sibling file objects.
* After this call, if no other changes have intervened,
* {@link #getOrder} on these files should return a list in the same order.
* Beyond the fact that this call may manipulate the <code>position</code> attributes
* of files in the folder, and may delete deprecated relative ordering attributes on the folder,
* the exact means of setting the order is unspecified.
* @param children a list of zero or more files (or folders); must all have the same {@link FileObject#getParent}
* @throws IllegalArgumentException in case there are duplicates, or nulls, or the files do not have a common parent
* @throws IOException if new file attributes to order the children cannot be written out
* @since org.openide.filesystems 7.2
*/
public static void setOrder(List<FileObject> children) throws IllegalArgumentException, IOException {
Ordering.setOrder(children);
}
/**
* Checks whether a change in a given file attribute would affect the result of {@link #getOrder}.
* @param event an attribute change event
* @return true if the attribute in question might affect the order of some folder
* @since org.openide.filesystems 7.2
*/
public static boolean affectsOrder(FileAttributeEvent event) {
return Ordering.affectsOrder(event);
}
/**
* Returns {@code FileObject} from the NetBeans default (system, configuration)
* filesystem or {@code null} if does not exist.
* If you wish to create the file/folder when it does not already exist,
* start with {@link #getConfigRoot} and use {@link #createData(FileObject, String)}
* or {@link #createFolder(FileObject, String)} methods.
* <p/>
* In environment with multiple contextual Lookups, the method may return different FileObject depending
* on what Lookup serves the executing thread. If the system-wide (user-independent) configuration
* is required instead, {@link #getSystemConfigFile} should be called instead. If an service instance is created based
* on the configuration, it is important to decide whether the service instance should live for each context
* independently (possibly with some private means of communication between instances/users) or all users
* should share the same instance. In the later case, {@link #getSystemConfigFile} must be used.
*
* @param path the path from the root of the NetBeans default (system, configuration)
* filesystem delimited by '/' or empty string to get root folder.
* @throws NullPointerException if the path is {@code null}
* @return a {@code FileObject} for given path in the NetBeans default (system, configuration)
* filesystem or {@code null} if does not exist
*
* @since org.openide.filesystems 7.19
* @since 9.5 support for multiuser environment
*/
@SuppressWarnings("deprecation")
public static FileObject getConfigFile(String path) {
Parameters.notNull("path", path); //NOI18N
Repository repo = Repository.getLocalRepository();
return (repo != null ? repo : Repository.getDefault()).getDefaultFileSystem().findResource(path);
}
/**
* Returns {@code FileObject} from the default filesystem, or {@code null} if the file does not exist.
* Unlike {@link #getConfigFile}, this call returns a FileObject from the system-wide configuration.
* Because default/config filesystem is used both for configuration and services, Lookup or service providers
* should use this method in preference to {@link #getConfigFile} to produce singleton services even
* in multiple context environment.
* <p/>
* With the default Lookup implementation, behaviour of {@code getSystemConfigFile} and {@link #getConfigFile}
* is identical.
*
* @param path the path from the root of the NetBeans default (system, configuration)
* filesystem delimited by '/' or empty string to get root folder.
* @throws NullPointerException if the path is {@code null}
* @return a {@code FileObject} for given path in the NetBeans default (system, configuration)
* filesystem or {@code null} if does not exist
* @since 9.5
*/
@SuppressWarnings("deprecation")
public static FileObject getSystemConfigFile(String path) {
Parameters.notNull("path", path); //NOI18N
return Repository.getDefault().getDefaultFileSystem().findResource(path);
}
/** Finds a config object under given path. The path contains the extension
* of a file e.g.:
* <pre>
* Actions/Edit/org-openide-actions-CopyAction.instance
* Services/Browsers/swing-browser.settings
* </pre>
* <p/>
* In multi-user setup, this method returns instance specific for the executing user.
* <b>Important<b>: it returns user-specific instance even though the object is configured in
* a XML layer, or system-wide configuration; still, the instance will be tied to the user-specific
* file as served by {@link #getConfigFile}.
*
* @param path path to .instance or .settings file
* @param type the requested type for given object
* @return either null or instance of requrested type
* @since 7.49
* @since 9.5 support for multi-user environment
*/
public static <T> T getConfigObject(String path, Class<T> type) {
FileObject fo = getConfigFile(path);
if (fo == null || fo.isFolder()) {
return null;
}
return NamedServicesProvider.getConfigObject(path, type);
}
/**
* Finds a config object under the given path, in system-wide configuration.
* In the default implementation, this method works just as {@link #getConfigObject}. In
* multi-user setups, this method should return an instance <b>shared</b> between
* potential users; in a sense, it works as {@link #getConfigObject} prior version 9.5
*
* @param path path to .instance or .settings file
* @param type the requested type for given object
* @return either null or instance of requrested type
* @since 9.5
*/
public static <T> T getSystemConfigObject(String path, Class<T> type) {
FileObject fo = getSystemConfigFile(path);
if (fo == null || fo.isFolder()) {
return null;
}
return NamedServicesProvider.getConfigObject(path, type);
}
/**
* Returns the root of the NetBeans default (system, configuration)
* filesystem. It returns configuration root using the current Repository, in the case
* that multiple Repository instances are created to support multiple execution contexts
* in the same JVM.
*
* @return a {@code FileObject} for the root of the NetBeans default (system, configuration)
* filesystem
* @since org.openide.filesystems 7.19
* @since 9.5 support for multiple local contexts
*/
public static FileObject getConfigRoot() {
return getConfigFile(""); //NOI18N
}
/**
* Returns the root of the NetBeans default (system, configuration)
* filesystem. Unlike {@link #getConfigRoot}, this method always provides the
* system-wide configuration root.
*
* @return a {@code FileObject} for the root of the NetBeans default (system, configuration)
* filesystem
* @since 9.5
*/
public static FileObject getSystemConfigRoot() {
return getSystemConfigFile("");
}
private static File wrapFileNoCanonicalize(File f) {
if (f instanceof NonCanonicalizingFile) {
return f;
} else if (f != null) {
return new NonCanonicalizingFile(f);
} else {
return null;
}
}
private static File[] wrapFilesNoCanonicalize(File[] fs) {
if (fs != null) {
for (int i = 0; i < fs.length; i++) {
fs[i] = wrapFileNoCanonicalize(fs[i]);
}
}
return fs;
}
private static final class NonCanonicalizingFile extends File {
public NonCanonicalizingFile(File orig) {
this(orig.getPath());
}
private NonCanonicalizingFile(String path) {
super(path);
}
private NonCanonicalizingFile(URI uri) {
super(uri);
}
@Override
public File getCanonicalFile() throws IOException {
return wrapFileNoCanonicalize(normalizeFile(super.getAbsoluteFile()));
}
@Override
public String getCanonicalPath() throws IOException {
return normalizeFile(super.getAbsoluteFile()).getAbsolutePath();
}
@Override
public File getParentFile() {
return wrapFileNoCanonicalize(super.getParentFile());
}
@Override
public File getAbsoluteFile() {
return wrapFileNoCanonicalize(super.getAbsoluteFile());
}
@Override
public File[] listFiles() {
return wrapFilesNoCanonicalize(super.listFiles());
}
@Override
public File[] listFiles(FileFilter filter) {
return wrapFilesNoCanonicalize(super.listFiles(filter));
}
@Override
public File[] listFiles(FilenameFilter filter) {
return wrapFilesNoCanonicalize(super.listFiles(filter));
}
}
private static FileSystem getDiskFileSystem() {
synchronized (FileUtil.class) {
return diskFileSystem;
}
}
private static void setDiskFileSystem(FileSystem fs) {
Object o = fs.getRoot().getAttribute("SupportsRefreshForNoPublicAPI");
if (o instanceof Boolean && ((Boolean) o).booleanValue()) {
synchronized (FileUtil.class) {
diskFileSystem = fs;
}
}
}
private static boolean isArchiveFileImpl(final URL url, final boolean strict) {
for (ArchiveRootProvider provider : getArchiveRootProviders()) {
if (provider.isArchiveFile(url, strict)) {
return true;
}
}
return false;
}
private static Iterable<? extends ArchiveRootProvider> getArchiveRootProviders() {
if (archiveRootProviderCache == null) {
Lookup.Result<ArchiveRootProvider> res = archiveRootProviders.get();
if (res == null) {
res = new ProxyLookup(
Lookups.singleton(new JarArchiveRootProvider()),
Lookup.getDefault()).lookupResult(ArchiveRootProvider.class);
if (archiveRootProviders.compareAndSet(null, res)) {
res = archiveRootProviders.get();
res.addLookupListener((ev) -> {
archiveRootProviderCache = null;
});
}
}
archiveRootProviderCache = new LinkedList<>(res.allInstances());
}
return archiveRootProviderCache;
}
private static volatile Iterable<? extends ArchiveRootProvider> archiveRootProviderCache;
private static final AtomicReference<Lookup.Result<ArchiveRootProvider>> archiveRootProviders = new AtomicReference<>();
}