--- a/openide.filesystems/apichanges.xml
+++ a/openide.filesystems/apichanges.xml
@@ -49,6 +49,23 @@
Filesystems API
+
+
+ Pluggable archive files support for FileUtil
+
+
+
+
+
+ To support JDK 9 image file as an archive the FileUtil's method
+ getArchiveFile
,getArchiveRoot
and isArchiveFile
+ are pluggable using a new SPI ArchiveFileProvider
.
+ In addition to these methods a new method isArchiveArtifact
was added.
+ This method can be used if given URL
points into an archive.
+
+
+
+
Support for multi-user environments
--- a/openide.filesystems/nbproject/project.properties
+++ a/openide.filesystems/nbproject/project.properties
@@ -41,7 +41,7 @@
# made subject to such option by the copyright holder.
javac.compilerargs=-Xlint -Xlint:-serial
-javac.source=1.7
+javac.source=1.8
module.jar.dir=core
javadoc.main.page=org/openide/filesystems/doc-files/api.html
javadoc.arch=${basedir}/arch.xml
--- a/openide.filesystems/src/META-INF/upgrade/FileUtil.hint
+++ a/openide.filesystems/src/META-INF/upgrade/FileUtil.hint
@@ -10,3 +10,23 @@
=>
$c.setCurrentDirectory($d)
;;
+
+//"jar".equals($url.getProtocol()) :: $url instanceof java.net.URL
+//=>
+//org.openide.filesystems.FileUtil.isArchiveArtifact($url)
+//;;
+
+//$url.getProtocol().equals("jar") :: $url instanceof java.net.URL
+//=>
+//org.openide.filesystems.FileUtil.isArchiveArtifact($url)
+//;;
+
+//"jar".equals($uri.getScheme()) :: $uri instanceof java.net.URI
+//=>
+//org.openide.filesystems.FileUtil.isArchiveArtifact($uri.toURL())
+//;;
+
+//$uri.getScheme().equals("jar") :: $uri instanceof java.net.URI
+//=>
+//org.openide.filesystems.FileUtil.isArchiveArtifact($uri.toURL())
+//;;
--- a/openide.filesystems/src/org/openide/filesystems/FileUtil.java
+++ a/openide.filesystems/src/org/openide/filesystems/FileUtil.java
@@ -56,10 +56,10 @@
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.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
@@ -71,22 +71,26 @@
import java.util.Set;
import java.util.Stack;
import java.util.StringTokenizer;
-import java.util.WeakHashMap;
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.
@@ -99,11 +103,6 @@
private static final Logger LOG = Logger.getLogger(FileUtil.class.getName());
- /** Normal header for ZIP files. */
- private static byte[] ZIP_HEADER_1 = {0x50, 0x4b, 0x03, 0x04};
- /** Also seems to be used at least in apisupport/project/test/unit/data/example-external-projects/suite3/nbplatform/random/modules/ext/stuff.jar; not known why */
- private static byte[] ZIP_HEADER_2 = {0x50, 0x4b, 0x05, 0x06};
-
/** transient attributes which should not be copied
* of type Set
*/
@@ -125,8 +124,6 @@
transientAttributes.add(MultiFileObject.WEIGHT_ATTRIBUTE); // NOI18N
}
- /** Cache for {@link #isArchiveFile(FileObject)}. */
- private static final Map archiveFileCache = new WeakHashMap();
private static FileSystem diskFileSystem;
static String toDebugString(File file) {
@@ -1835,35 +1832,41 @@
* 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 ZIP- (or JAR-) format 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) {
- URL archiveURL = URLMapper.findURL(fo, URLMapper.EXTERNAL);
-
- if (archiveURL == null) {
- return null;
+ for (ArchiveRootProvider provider : getArchiveRootProviders()) {
+ if (provider.isArchiveFile(fo, false)) {
+ final FileObject root = provider.getArchiveRoot(fo);
+ if (root != null) {
+ return root;
+ }
+ }
}
-
- return URLMapper.findFileObject(getArchiveRoot(archiveURL));
+ 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 ZIP- (or JAR-) format archive file
- * @return the jar
-protocol URL of the root of the archive
+ * @param url of a java archive file, by default ZIP and JAR are supported
+ * @return the archive (eg. jar
) protocol URL of the root of the archive.
* @since 4.48
*/
public static URL getArchiveRoot(URL url) {
- try {
- // XXX TBD whether the url should ever be escaped...
- return new URL("jar:" + url + "!/"); // NOI18N
- } catch (MalformedURLException e) {
- throw new AssertionError(e);
+ 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);
}
/**
@@ -1871,161 +1874,107 @@
* FileObject given by the parameter.
* Remember that any path within the archive is discarded
* so you may need to check for non-root entries.
- * @param fo a file in a JAR filesystem
+ * @param fo a file in an archive filesystem
* @return the file corresponding to the archive itself,
* or null if fo
is not an archive entry
* @since 4.48
*/
public static FileObject getArchiveFile(FileObject fo) {
Parameters.notNull("fo", fo); //NOI18N
- try {
- FileSystem fs = fo.getFileSystem();
-
- if (fs instanceof JarFileSystem) {
- File jarFile = ((JarFileSystem) fs).getJarFile();
-
- return toFileObject(jarFile);
+ for (ArchiveRootProvider provider : getArchiveRootProviders()) {
+ if (provider.isArchiveArtifact(fo)) {
+ final FileObject file = provider.getArchiveFile(fo);
+ if (file != null) {
+ return file;
+ }
}
- } catch (FileStateInvalidException e) {
- Exceptions.printStackTrace(e);
}
-
return null;
}
/**
* Returns the URL of the archive file containing the file
- * referred to by a jar
-protocol URL.
+ * referred to by an archive (eg. jar
) protocol URL.
* Remember 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 a
- * jar
-protocol URL containing !/
+ * @return the embedded archive URL, or null if the URL is not an
+ * archive protocol URL containing !/
* @since 4.48
*/
public static URL getArchiveFile(URL url) {
- String protocol = url.getProtocol();
-
- if ("jar".equals(protocol)) { //NOI18N
-
- String path = url.getPath();
- int index = path.indexOf("!/"); //NOI18N
-
- if (index >= 0) {
- String jarPath = null;
- try {
- jarPath = path.substring(0, index);
- if (jarPath.indexOf("file://") > -1 && jarPath.indexOf("file:////") == -1) { //NOI18N
- /* Replace because JDK application classloader wrongly recognizes UNC paths. */
- jarPath = jarPath.replaceFirst("file://", "file:////"); //NOI18N
- }
- return new URL(jarPath);
-
- } catch (MalformedURLException mue) {
- LOG.log(
- Level.WARNING,
- "Invalid URL ({0}): {1}, jarPath: {2}", //NOI18N
- new Object[] {
- mue.getMessage(),
- url.toExternalForm(),
- jarPath
- });
+ 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 JAR or ZIP archive.
+ * 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 ZIP-format archive
+ * @return true if the file looks like a java archive
* @since 4.48
*/
public static boolean isArchiveFile(FileObject fo) {
Parameters.notNull("fileObject", fo); //NOI18N
-
- if (!fo.isValid()) {
- return isArchiveFile(fo.getPath());
+ for (ArchiveRootProvider provider : getArchiveRootProviders()) {
+ if (provider.isArchiveFile(fo, true)) {
+ return true;
+ }
}
- // XXX Special handling of virtual file objects: try to determine it using its name, but don't cache the
- // result; when the file is checked out the more correct method can be used
- if (fo.isVirtual()) {
- return isArchiveFile(fo.getPath());
- }
-
- if (fo.isFolder()) {
- return false;
- }
-
- // First check the cache.
- Boolean b = archiveFileCache.get(fo);
-
- if (b == null) {
- // Need to check it.
- try {
- InputStream in = fo.getInputStream();
-
- try {
- byte[] buffer = new byte[4];
- int len = in.read(buffer, 0, 4);
-
- if (len == 4) {
- // Got a header, see if it is a ZIP file.
- b = Boolean.valueOf(Arrays.equals(ZIP_HEADER_1, buffer) || Arrays.equals(ZIP_HEADER_2, buffer));
- } else {
- //If the length is less than 4, it can be either
- //broken (empty) archive file or other empty file.
- //Return false and don't cache it, when the archive
- //file will be written and closed its length will change
- return false;
- }
- } finally {
- in.close();
- }
- } catch (IOException ioe) {
- // #160507 - ignore exception (e.g. permission denied)
- LOG.log(Level.FINE, null, ioe);
- }
-
- if (b == null) {
- b = isArchiveFile(fo.getPath());
- }
-
- archiveFileCache.put(fo, b);
- }
-
- return b.booleanValue();
+ return false;
}
/**
- * Tests if a URL represents a JAR or ZIP archive.
+ * 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 ZIP-format archive
+ * @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);
+ }
- if ("jar".equals(url.getProtocol())) { //NOI18N
+ /**
+ * 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.6
+ */
+ public static boolean isArchiveArtifact(FileObject fo) {
+ Parameters.notNull("fileObject", fo); //NOI18N
+ for (ArchiveRootProvider provider : getArchiveRootProviders()) {
+ if (provider.isArchiveArtifact(fo)) {
+ return true;
+ }
+ }
+ return false;
+ }
- //Already inside archive, return false
- 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.6
+ */
+ public static boolean isArchiveArtifact(URL url) {
+ Parameters.notNull("url", url); //NOI18N
+ for (ArchiveRootProvider provider : getArchiveRootProviders()) {
+ if (provider.isArchiveArtifact(url)) {
+ return true;
+ }
}
-
- FileObject fo = URLMapper.findFileObject(url);
-
- if ((fo != null) && !fo.isVirtual()) {
- if (LOG.isLoggable(Level.FINEST)) {
- LOG.log(Level.FINEST, "isArchiveFile_FILE_RESOLVED", fo); //NOI18N, used by FileUtilTest.testIsArchiveFileRace
- }
- return isArchiveFile(fo);
- } else {
- return isArchiveFile(url.getPath());
- }
+ return false;
}
/**
@@ -2048,7 +1997,7 @@
u = BaseUtilities.toURI(entry).toURL();
isDir = entry.isDirectory();
} while (wasDir ^ isDir);
- if (isArchiveFile(u) || entry.isFile() && entry.length() < 4) {
+ if (isArchiveFileImpl(u, false)) {
return getArchiveRoot(u);
} else if (isDir) {
assert u.toExternalForm().endsWith("/"); //NOI18N
@@ -2078,9 +2027,23 @@
* @since org.openide.filesystems 7.8
*/
public static File archiveOrDirForURL(URL entry) {
- String u = entry.toString();
- if (u.startsWith("jar:file:") && u.endsWith("!/")) { // NOI18N
- return BaseUtilities.toFile(URI.create(u.substring(4, u.length() - 2)));
+ 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 {
@@ -2361,13 +2324,27 @@
}
}
- /**
- * Tests if a non existent path represents a file.
- * @param path to be tested, separated by '/'.
- * @return true if the file has '.' after last '/'.
- */
- private static boolean isArchiveFile (final String path) {
- int index = path.lastIndexOf('.'); //NOI18N
- return (index != -1) && (index > path.lastIndexOf('/') + 1); //NOI18N
+ 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() {
+ Lookup.Result 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();
+ }
+ }
+ return res.allInstances();
+ }
+
+ private static final AtomicReference> archiveRootProviders = new AtomicReference<>();
}
--- a/openide.filesystems/src/org/openide/filesystems/JarArchiveRootProvider.java
+++ a/openide.filesystems/src/org/openide/filesystems/JarArchiveRootProvider.java
@@ -0,0 +1,214 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2015 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
+ * Other names may be trademarks of their respective owners.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common
+ * Development and Distribution License("CDDL") (collectively, the
+ * "License"). You may not use this file except in compliance with the
+ * License. You can obtain a copy of the License at
+ * http://www.netbeans.org/cddl-gplv2.html
+ * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
+ * specific language governing permissions and limitations under the
+ * License. When distributing the software, include this License Header
+ * Notice in each file and include the License file at
+ * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the GPL Version 2 section of the License file that
+ * accompanied this code. If applicable, add the following below the
+ * License Header, with the fields enclosed by brackets [] replaced by
+ * your own identifying information:
+ * "Portions Copyrighted [year] [name of copyright owner]"
+ *
+ * If you wish your version of this file to be governed by only the CDDL
+ * or only the GPL Version 2, indicate your decision by adding
+ * "[Contributor] elects to include this software in this distribution
+ * under the [CDDL or GPL Version 2] license." If you do not indicate a
+ * single choice of license, a recipient has the option to distribute
+ * your version of this file under either the CDDL, the GPL Version 2 or
+ * to extend the choice of license to its licensees as provided above.
+ * However, if you add GPL Version 2 code and therefore, elected the GPL
+ * Version 2 license, then the option applies only if the new code is
+ * made subject to such option by the copyright holder.
+ *
+ * Contributor(s):
+ *
+ * Portions Copyrighted 2015 Sun Microsystems, Inc.
+ */
+package org.openide.filesystems;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.openide.filesystems.spi.ArchiveRootProvider;
+import org.openide.util.Exceptions;
+
+/**
+ *
+ * @author Tomas Zezula
+ */
+final class JarArchiveRootProvider implements ArchiveRootProvider {
+ private static final String PROTOCOL = "jar"; //NOI18N
+ /** Normal header for ZIP files. */
+ private static byte[] ZIP_HEADER_1 = {0x50, 0x4b, 0x03, 0x04};
+ /** Also seems to be used at least in apisupport/project/test/unit/data/example-external-projects/suite3/nbplatform/random/modules/ext/stuff.jar; not known why */
+ private static byte[] ZIP_HEADER_2 = {0x50, 0x4b, 0x05, 0x06};
+ private static final Logger LOG = Logger.getLogger(JarArchiveRootProvider.class.getName());
+ /** Cache for {@link #isArchiveFile(FileObject)}. */
+ private static final Map*@GuardedBy("archiveFileCache")*/FileObject, Boolean> archiveFileCache = Collections.synchronizedMap(new WeakHashMap());
+
+ @Override
+ public boolean isArchiveFile(URL url, boolean strict) {
+ if (PROTOCOL.equals(url.getProtocol())) { //NOI18N
+ //Already inside archive, return false
+ return false;
+ }
+ FileObject fo = URLMapper.findFileObject(url);
+ if ((fo != null) && !fo.isVirtual()) {
+ if (LOG.isLoggable(Level.FINEST)) {
+ LOG.log(Level.FINEST, "isArchiveFile_FILE_RESOLVED", fo); //NOI18N, used by FileUtilTest.testIsArchiveFileRace
+ }
+ return isArchiveFile(fo, strict);
+ } else {
+ return isArchiveFile(url.getPath());
+ }
+ }
+
+ @Override
+ public boolean isArchiveFile(FileObject fo, boolean strict) {
+ if (!fo.isValid()) {
+ return isArchiveFile(fo.getPath());
+ }
+ // XXX Special handling of virtual file objects: try to determine it using its name, but don't cache the
+ // result; when the file is checked out the more correct method can be used
+ if (fo.isVirtual()) {
+ return isArchiveFile(fo.getPath());
+ }
+
+ if (fo.isFolder()) {
+ return false;
+ }
+
+ // First check the cache.
+ Boolean b = archiveFileCache.get(fo);
+ if (b == null) {
+ // Need to check it.
+ try {
+ InputStream in = fo.getInputStream();
+
+ try {
+ byte[] buffer = new byte[4];
+ int len = in.read(buffer, 0, 4);
+
+ if (len == 4) {
+ // Got a header, see if it is a ZIP file.
+ b = Boolean.valueOf(Arrays.equals(ZIP_HEADER_1, buffer) || Arrays.equals(ZIP_HEADER_2, buffer));
+ } else {
+ //If the length is less than 4, it can be either
+ //broken (empty) archive file or other empty file.
+ //Return false and don't cache it, when the archive
+ //file will be written and closed its length will change
+ return !strict;
+ }
+ } finally {
+ in.close();
+ }
+ } catch (IOException ioe) {
+ // #160507 - ignore exception (e.g. permission denied)
+ LOG.log(Level.FINE, null, ioe);
+ }
+ if (b == null) {
+ b = isArchiveFile(fo.getPath());
+ }
+ archiveFileCache.put(fo, b);
+ }
+ return b.booleanValue();
+ }
+
+
+ @Override
+ public boolean isArchiveArtifact(URL url) {
+ return PROTOCOL.equals(url.getProtocol());
+ }
+
+ @Override
+ public URL getArchiveFile(URL url) {
+ String protocol = url.getProtocol();
+
+ if (PROTOCOL.equals(protocol)) { //NOI18N
+
+ String path = url.getPath();
+ int index = path.indexOf("!/"); //NOI18N
+
+ if (index >= 0) {
+ String jarPath = null;
+ try {
+ jarPath = path.substring(0, index);
+ if (jarPath.indexOf("file://") > -1 && jarPath.indexOf("file:////") == -1) { //NOI18N
+ /* Replace because JDK application classloader wrongly recognizes UNC paths. */
+ jarPath = jarPath.replaceFirst("file://", "file:////"); //NOI18N
+ }
+ return new URL(jarPath);
+
+ } catch (MalformedURLException mue) {
+ LOG.log(
+ Level.WARNING,
+ "Invalid URL ({0}): {1}, jarPath: {2}", //NOI18N
+ new Object[] {
+ mue.getMessage(),
+ url.toExternalForm(),
+ jarPath
+ });
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public FileObject getArchiveFile(FileObject fo) {
+ try {
+ final FileSystem fs = fo.getFileSystem();
+ if (fs instanceof JarFileSystem) {
+ final File jarFile = ((JarFileSystem) fs).getJarFile();
+ return FileUtil.toFileObject(jarFile);
+ }
+ } catch (FileStateInvalidException e) {
+ Exceptions.printStackTrace(e);
+ }
+ return null;
+ }
+
+ @Override
+ public URL getArchiveRoot(final URL url) {
+ try {
+ // XXX TBD whether the url should ever be escaped...
+ return new URL("jar:" + url + "!/"); // NOI18N
+ } catch (MalformedURLException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Tests if a non existent path represents a file.
+ * @param path to be tested, separated by '/'.
+ * @return true if the file has '.' after last '/'.
+ */
+ private static boolean isArchiveFile (final String path) {
+ int index = path.lastIndexOf('.'); //NOI18N
+ return (index != -1) && (index > path.lastIndexOf('/') + 1); //NOI18N
+ }
+
+}
--- a/openide.filesystems/src/org/openide/filesystems/spi/ArchiveRootProvider.java
+++ a/openide.filesystems/src/org/openide/filesystems/spi/ArchiveRootProvider.java
@@ -0,0 +1,156 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2015 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
+ * Other names may be trademarks of their respective owners.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common
+ * Development and Distribution License("CDDL") (collectively, the
+ * "License"). You may not use this file except in compliance with the
+ * License. You can obtain a copy of the License at
+ * http://www.netbeans.org/cddl-gplv2.html
+ * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
+ * specific language governing permissions and limitations under the
+ * License. When distributing the software, include this License Header
+ * Notice in each file and include the License file at
+ * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the GPL Version 2 section of the License file that
+ * accompanied this code. If applicable, add the following below the
+ * License Header, with the fields enclosed by brackets [] replaced by
+ * your own identifying information:
+ * "Portions Copyrighted [year] [name of copyright owner]"
+ *
+ * If you wish your version of this file to be governed by only the CDDL
+ * or only the GPL Version 2, indicate your decision by adding
+ * "[Contributor] elects to include this software in this distribution
+ * under the [CDDL or GPL Version 2] license." If you do not indicate a
+ * single choice of license, a recipient has the option to distribute
+ * your version of this file under either the CDDL, the GPL Version 2 or
+ * to extend the choice of license to its licensees as provided above.
+ * However, if you add GPL Version 2 code and therefore, elected the GPL
+ * Version 2 license, then the option applies only if the new code is
+ * made subject to such option by the copyright holder.
+ *
+ * Contributor(s):
+ *
+ * Portions Copyrighted 2015 Sun Microsystems, Inc.
+ */
+package org.openide.filesystems.spi;
+
+import java.net.URL;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.filesystems.URLMapper;
+
+/**
+ * A possibility to plug a support for java archives into FileUtil.
+ * The interface is used by {@link FileUtil.isArchiveArtifact}, {@link FileUtil.isArchiveFile},
+ * {@link FileUtil.getArchiveRoot}, {@link FileUtil.getArchiveFile}.
+ * The implementations are registered in global lookup.
+ * @author Tomas Zezula
+ * @since 9.6
+ */
+public interface ArchiveRootProvider {
+
+ /**
+ * Tests if a file represents an java archive.
+ * @param url the file to be tested
+ * @param strict when false the detection may not be precise, for example
+ * an empty archive missing the archive header is treated as an archive
+ * @return true if the file looks like an archive
+ */
+ boolean isArchiveFile(URL url, boolean strict);
+
+ /**
+ * Tests if a file represents an java archive.
+ * The default implementation delegates to {@link ArchiveRootProvider#isArchiveFile(URL, boolean)},
+ * it can be overridden by an implementation in more efficient way.
+ * @param fo the file to be tested
+ * @param strict when false the detection may not be precise, for example
+ * an empty archive missing the archive header is treated as an archive
+ * @return true if the file looks like an archive
+ */
+ default boolean isArchiveFile(FileObject fo, boolean strict) {
+ final URL url = URLMapper.findURL(fo, URLMapper.EXTERNAL);
+ return url == null ? false : isArchiveFile(url, strict);
+ }
+
+ /**
+ * 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
+ */
+ boolean isArchiveArtifact(URL url);
+
+ /**
+ * Tests if an file is inside an archive.
+ * The default implementation delegates to {@link ArchiveRootProvider#isArchiveArtifact(URL)},
+ * it can be overridden by an implementation in more efficient way.
+ * @param fo the file to be tested
+ * @return true if the file is inside an archive
+ */
+ default boolean isArchiveArtifact(FileObject fo) {
+ final URL url = URLMapper.findURL(fo, URLMapper.EXTERNAL);
+ return url == null ? false : isArchiveArtifact(url);
+ }
+
+ /**
+ * Returns the URL of the archive file containing the file
+ * referred to by a archive-protocol URL.
+ * Remember 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 !/
+ */
+ URL getArchiveFile(URL url);
+
+ /**
+ * Returns a FileObject representing an archive file containing the
+ * FileObject given by the parameter.
+ * Remember that any path within the archive is discarded
+ * so you may need to check for non-root entries.
+ * The default implementation delegates to {@link ArchiveRootProvider#getArchiveFile(URL)},
+ * it can be overridden by an implementation in more efficient way.
+ * @param fo a file in a archive filesystem
+ * @return the file corresponding to the archive itself,
+ * or null if fo
is not an archive entry
+ */
+ default FileObject getArchiveFile(FileObject fo) {
+ final URL rootURL = URLMapper.findURL(fo, URLMapper.EXTERNAL);
+ if (rootURL == null) {
+ return null;
+ }
+ return URLMapper.findFileObject(FileUtil.getArchiveFile(rootURL));
+ }
+
+ /**
+ * Returns an 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 an java archive file
+ * @return the archive-protocol URL of the root of the archive
+ */
+ URL getArchiveRoot(URL url);
+
+ /**
+ * 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.
+ * The default implementation delegates to {@link ArchiveRootProvider#getArchiveRoot(URL)},
+ * it can be overridden by an implementation in more efficient way.
+ * @param fo an java archive file
+ * @return a virtual archive root folder, or null if the file is not actually an archive
+ */
+ default FileObject getArchiveRoot(FileObject fo) {
+ final URL archiveURL = URLMapper.findURL(fo, URLMapper.EXTERNAL);
+ if (archiveURL == null) {
+ return null;
+ }
+ return URLMapper.findFileObject(FileUtil.getArchiveRoot(archiveURL));
+ }
+}
--- a/openide.filesystems/test/unit/src/org/openide/filesystems/FileUtilTest.java
+++ a/openide.filesystems/test/unit/src/org/openide/filesystems/FileUtilTest.java
@@ -276,7 +276,7 @@
final File testFile = new File (wd,"test.jar"); //NOI18N
FileUtil.createData(testFile);
- final Logger log = Logger.getLogger(FileUtil.class.getName());
+ final Logger log = Logger.getLogger(JarArchiveRootProvider.class.getName());
log.setLevel(Level.FINEST);
final Handler handler = new Handler() {
@Override