/* * Copyright 2013 Google Inc. * * Licensed 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 com.google.common.jimfs; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.jimfs.Feature.FILE_CHANNEL; import static com.google.common.jimfs.Feature.LINKS; import static com.google.common.jimfs.Feature.SECURE_DIRECTORY_STREAM; import static com.google.common.jimfs.Feature.SYMBOLIC_LINKS; import static com.google.common.jimfs.PathNormalization.CASE_FOLD_ASCII; import static com.google.common.jimfs.PathNormalization.NFC; import static com.google.common.jimfs.PathNormalization.NFD; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import java.nio.channels.FileChannel; import java.nio.file.FileSystem; import java.nio.file.InvalidPathException; import java.nio.file.SecureDirectoryStream; import java.nio.file.WatchService; import java.nio.file.attribute.BasicFileAttributeView; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import org.checkerframework.checker.nullness.compatqual.NullableDecl; /** * Immutable configuration for an in-memory file system. A {@code Configuration} is passed to a * method in {@link Jimfs} such as {@link Jimfs#newFileSystem(Configuration)} to create a new {@link * FileSystem} instance. * * @author Colin Decker */ public final class Configuration { /** * Returns the default configuration for a UNIX-like file system. A file system created with this * configuration: * * * *

To create a modified version of this configuration, such as to include the full set of UNIX * file attribute views, {@linkplain #toBuilder() create a builder}. * *

Example: * *

   *   Configuration config = Configuration.unix().toBuilder()
   *       .setAttributeViews("basic", "owner", "posix", "unix")
   *       .setWorkingDirectory("/home/user")
   *       .build();  
*/ public static Configuration unix() { return UnixHolder.UNIX; } private static final class UnixHolder { private static final Configuration UNIX = Configuration.builder(PathType.unix()) .setDisplayName("Unix") .setRoots("/") .setWorkingDirectory("/work") .setAttributeViews("basic") .setSupportedFeatures(LINKS, SYMBOLIC_LINKS, SECURE_DIRECTORY_STREAM, FILE_CHANNEL) .build(); } /** * Returns the default configuration for a Mac OS X-like file system. * *

The primary differences between this configuration and the default {@link #unix()} * configuration are that this configuration does Unicode normalization on the display and * canonical forms of filenames and does case insensitive file lookup. * *

A file system created with this configuration: * *

* *

To create a modified version of this configuration, such as to include the full set of UNIX * file attribute views or to use full Unicode case insensitivity, {@linkplain #toBuilder() create * a builder}. * *

Example: * *

   *   Configuration config = Configuration.osX().toBuilder()
   *       .setAttributeViews("basic", "owner", "posix", "unix")
   *       .setNameCanonicalNormalization(NFD, CASE_FOLD_UNICODE)
   *       .setWorkingDirectory("/Users/user")
   *       .build();  
*/ public static Configuration osX() { return OsxHolder.OS_X; } private static final class OsxHolder { private static final Configuration OS_X = unix().toBuilder() .setDisplayName("OSX") .setNameDisplayNormalization(NFC) // matches JDK 1.7u40+ behavior .setNameCanonicalNormalization(NFD, CASE_FOLD_ASCII) // NFD is default in HFS+ .setSupportedFeatures(LINKS, SYMBOLIC_LINKS, FILE_CHANNEL) .build(); } /** * Returns the default configuration for a Windows-like file system. A file system created with * this configuration: * * * *

To create a modified version of this configuration, such as to include the full set of * Windows file attribute views or to use full Unicode case insensitivity, {@linkplain * #toBuilder() create a builder}. * *

Example: * *

   *   Configuration config = Configuration.windows().toBuilder()
   *       .setAttributeViews("basic", "owner", "dos", "acl", "user")
   *       .setNameCanonicalNormalization(CASE_FOLD_UNICODE)
   *       .setWorkingDirectory("C:\\Users\\user") // or "C:/Users/user"
   *       .build();  
*/ public static Configuration windows() { return WindowsHolder.WINDOWS; } private static final class WindowsHolder { private static final Configuration WINDOWS = Configuration.builder(PathType.windows()) .setDisplayName("Windows") .setRoots("C:\\") .setWorkingDirectory("C:\\work") .setNameCanonicalNormalization(CASE_FOLD_ASCII) .setPathEqualityUsesCanonicalForm(true) // matches real behavior of WindowsPath .setAttributeViews("basic") .setSupportedFeatures(LINKS, SYMBOLIC_LINKS, FILE_CHANNEL) .build(); } /** * Returns a default configuration appropriate to the current operating system. * *

More specifically, if the operating system is Windows, {@link Configuration#windows()} is * returned; if the operating system is Mac OS X, {@link Configuration#osX()} is returned; * otherwise, {@link Configuration#unix()} is returned. * *

This is the configuration used by the {@code Jimfs.newFileSystem} methods that do not take a * {@code Configuration} parameter. * * @since 1.1 */ public static Configuration forCurrentPlatform() { String os = System.getProperty("os.name"); if (os.contains("Windows")) { return windows(); } else if (os.contains("OS X")) { return osX(); } else { return unix(); } } /** Creates a new mutable {@link Configuration} builder using the given path type. */ public static Builder builder(PathType pathType) { return new Builder(pathType); } // Path configuration final PathType pathType; final ImmutableSet nameDisplayNormalization; final ImmutableSet nameCanonicalNormalization; final boolean pathEqualityUsesCanonicalForm; // Disk configuration final int blockSize; final long maxSize; final long maxCacheSize; // Attribute configuration final ImmutableSet attributeViews; final ImmutableSet attributeProviders; final ImmutableMap defaultAttributeValues; // Watch service final WatchServiceConfiguration watchServiceConfig; // Other final ImmutableSet roots; final String workingDirectory; final ImmutableSet supportedFeatures; private final String displayName; /** Creates an immutable configuration object from the given builder. */ private Configuration(Builder builder) { this.pathType = builder.pathType; this.nameDisplayNormalization = builder.nameDisplayNormalization; this.nameCanonicalNormalization = builder.nameCanonicalNormalization; this.pathEqualityUsesCanonicalForm = builder.pathEqualityUsesCanonicalForm; this.blockSize = builder.blockSize; this.maxSize = builder.maxSize; this.maxCacheSize = builder.maxCacheSize; this.attributeViews = builder.attributeViews; this.attributeProviders = builder.attributeProviders == null ? ImmutableSet.of() : ImmutableSet.copyOf(builder.attributeProviders); this.defaultAttributeValues = builder.defaultAttributeValues == null ? ImmutableMap.of() : ImmutableMap.copyOf(builder.defaultAttributeValues); this.watchServiceConfig = builder.watchServiceConfig; this.roots = builder.roots; this.workingDirectory = builder.workingDirectory; this.supportedFeatures = builder.supportedFeatures; this.displayName = builder.displayName; } @Override public String toString() { if (displayName != null) { return MoreObjects.toStringHelper(this).addValue(displayName).toString(); } MoreObjects.ToStringHelper helper = MoreObjects.toStringHelper(this) .add("pathType", pathType) .add("roots", roots) .add("supportedFeatures", supportedFeatures) .add("workingDirectory", workingDirectory); if (!nameDisplayNormalization.isEmpty()) { helper.add("nameDisplayNormalization", nameDisplayNormalization); } if (!nameCanonicalNormalization.isEmpty()) { helper.add("nameCanonicalNormalization", nameCanonicalNormalization); } helper .add("pathEqualityUsesCanonicalForm", pathEqualityUsesCanonicalForm) .add("blockSize", blockSize) .add("maxSize", maxSize); if (maxCacheSize != Builder.DEFAULT_MAX_CACHE_SIZE) { helper.add("maxCacheSize", maxCacheSize); } if (!attributeViews.isEmpty()) { helper.add("attributeViews", attributeViews); } if (!attributeProviders.isEmpty()) { helper.add("attributeProviders", attributeProviders); } if (!defaultAttributeValues.isEmpty()) { helper.add("defaultAttributeValues", defaultAttributeValues); } if (watchServiceConfig != WatchServiceConfiguration.DEFAULT) { helper.add("watchServiceConfig", watchServiceConfig); } return helper.toString(); } /** * Returns a new mutable builder that initially contains the same settings as this configuration. */ public Builder toBuilder() { return new Builder(this); } /** Mutable builder for {@link Configuration} objects. */ public static final class Builder { /** 8 KB. */ public static final int DEFAULT_BLOCK_SIZE = 8192; /** 4 GB. */ public static final long DEFAULT_MAX_SIZE = 4L * 1024 * 1024 * 1024; /** Equal to the configured max size. */ public static final long DEFAULT_MAX_CACHE_SIZE = -1; // Path configuration private final PathType pathType; private ImmutableSet nameDisplayNormalization = ImmutableSet.of(); private ImmutableSet nameCanonicalNormalization = ImmutableSet.of(); private boolean pathEqualityUsesCanonicalForm = false; // Disk configuration private int blockSize = DEFAULT_BLOCK_SIZE; private long maxSize = DEFAULT_MAX_SIZE; private long maxCacheSize = DEFAULT_MAX_CACHE_SIZE; // Attribute configuration private ImmutableSet attributeViews = ImmutableSet.of(); private Set attributeProviders = null; private Map defaultAttributeValues; // Watch service private WatchServiceConfiguration watchServiceConfig = WatchServiceConfiguration.DEFAULT; // Other private ImmutableSet roots = ImmutableSet.of(); private String workingDirectory; private ImmutableSet supportedFeatures = ImmutableSet.of(); private String displayName; private Builder(PathType pathType) { this.pathType = checkNotNull(pathType); } private Builder(Configuration configuration) { this.pathType = configuration.pathType; this.nameDisplayNormalization = configuration.nameDisplayNormalization; this.nameCanonicalNormalization = configuration.nameCanonicalNormalization; this.pathEqualityUsesCanonicalForm = configuration.pathEqualityUsesCanonicalForm; this.blockSize = configuration.blockSize; this.maxSize = configuration.maxSize; this.maxCacheSize = configuration.maxCacheSize; this.attributeViews = configuration.attributeViews; this.attributeProviders = configuration.attributeProviders.isEmpty() ? null : new HashSet<>(configuration.attributeProviders); this.defaultAttributeValues = configuration.defaultAttributeValues.isEmpty() ? null : new HashMap<>(configuration.defaultAttributeValues); this.watchServiceConfig = configuration.watchServiceConfig; this.roots = configuration.roots; this.workingDirectory = configuration.workingDirectory; this.supportedFeatures = configuration.supportedFeatures; // displayName intentionally not copied from the Configuration } /** * Sets the normalizations that will be applied to the display form of filenames. The display * form is used in the {@code toString()} of {@code Path} objects. */ public Builder setNameDisplayNormalization(PathNormalization first, PathNormalization... more) { this.nameDisplayNormalization = checkNormalizations(Lists.asList(first, more)); return this; } /** * Returns the normalizations that will be applied to the canonical form of filenames in the * file system. The canonical form is used to determine the equality of two filenames when * performing a file lookup. */ public Builder setNameCanonicalNormalization( PathNormalization first, PathNormalization... more) { this.nameCanonicalNormalization = checkNormalizations(Lists.asList(first, more)); return this; } private ImmutableSet checkNormalizations( List normalizations) { PathNormalization none = null; PathNormalization normalization = null; PathNormalization caseFold = null; for (PathNormalization n : normalizations) { checkNotNull(n); checkNormalizationNotSet(n, none); switch (n) { case NONE: none = n; break; case NFC: case NFD: checkNormalizationNotSet(n, normalization); normalization = n; break; case CASE_FOLD_UNICODE: case CASE_FOLD_ASCII: checkNormalizationNotSet(n, caseFold); caseFold = n; break; default: throw new AssertionError(); // there are no other cases } } if (none != null) { return ImmutableSet.of(); } return Sets.immutableEnumSet(normalizations); } private static void checkNormalizationNotSet( PathNormalization n, @NullableDecl PathNormalization set) { if (set != null) { throw new IllegalArgumentException( "can't set normalization " + n + ": normalization " + set + " already set"); } } /** * Sets whether {@code Path} objects in the file system use the canonical form (true) or the * display form (false) of filenames for determining equality of two paths. * *

The default is false. */ public Builder setPathEqualityUsesCanonicalForm(boolean useCanonicalForm) { this.pathEqualityUsesCanonicalForm = useCanonicalForm; return this; } /** * Sets the block size (in bytes) for the file system to use. All regular files will be * allocated blocks of the given size, so this is the minimum granularity for file size. * *

The default is 8192 bytes (8 KB). */ public Builder setBlockSize(int blockSize) { checkArgument(blockSize > 0, "blockSize (%s) must be positive", blockSize); this.blockSize = blockSize; return this; } /** * Sets the maximum size (in bytes) for the file system's in-memory file storage. This maximum * size determines the maximum number of blocks that can be allocated to regular files, so it * should generally be a multiple of the {@linkplain #setBlockSize(int) block size}. The actual * maximum size will be the nearest multiple of the block size that is less than or equal to the * given size. * *

Note: The in-memory file storage will not be eagerly initialized to this size, so * it won't use more memory than is needed for the files you create. Also note that in addition * to this limit, you will of course be limited by the amount of heap space available to the JVM * and the amount of heap used by other objects, both in the file system and elsewhere. * *

The default is 4 GB. */ public Builder setMaxSize(long maxSize) { checkArgument(maxSize > 0, "maxSize (%s) must be positive", maxSize); this.maxSize = maxSize; return this; } /** * Sets the maximum amount of unused space (in bytes) in the file system's in-memory file * storage that should be cached for reuse. By default, this will be equal to the {@linkplain * #setMaxSize(long) maximum size} of the storage, meaning that all space that is freed when * files are truncated or deleted is cached for reuse. This helps to avoid lots of garbage * collection when creating and deleting many files quickly. This can be set to 0 to disable * caching entirely (all freed blocks become available for garbage collection) or to some other * number to put an upper bound on the maximum amount of unused space the file system will keep * around. * *

Like the maximum size, the actual value will be the closest multiple of the block size * that is less than or equal to the given size. */ public Builder setMaxCacheSize(long maxCacheSize) { checkArgument(maxCacheSize >= 0, "maxCacheSize (%s) may not be negative", maxCacheSize); this.maxCacheSize = maxCacheSize; return this; } /** * Sets the attribute views the file system should support. By default, the following views may * be specified: * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
NameView InterfaceAttributes Interface
{@code "basic"}{@link java.nio.file.attribute.BasicFileAttributeView BasicFileAttributeView}{@link java.nio.file.attribute.BasicFileAttributes BasicFileAttributes}
{@code "owner"}{@link java.nio.file.attribute.FileOwnerAttributeView FileOwnerAttributeView}--
{@code "posix"}{@link java.nio.file.attribute.PosixFileAttributeView PosixFileAttributeView}{@link java.nio.file.attribute.PosixFileAttributes PosixFileAttributes}
{@code "unix"}----
{@code "dos"}{@link java.nio.file.attribute.DosFileAttributeView DosFileAttributeView}{@link java.nio.file.attribute.DosFileAttributes DosFileAttributes}
{@code "acl"}{@link java.nio.file.attribute.AclFileAttributeView AclFileAttributeView}--
{@code "user"}{@link java.nio.file.attribute.UserDefinedFileAttributeView UserDefinedFileAttributeView}--
* *

If any other views should be supported, attribute providers for those views must be * {@linkplain #addAttributeProvider(AttributeProvider) added}. */ public Builder setAttributeViews(String first, String... more) { this.attributeViews = ImmutableSet.copyOf(Lists.asList(first, more)); return this; } /** Adds an attribute provider for a custom view for the file system to support. */ public Builder addAttributeProvider(AttributeProvider provider) { checkNotNull(provider); if (attributeProviders == null) { attributeProviders = new HashSet<>(); } attributeProviders.add(provider); return this; } /** * Sets the default value to use for the given file attribute when creating new files. The * attribute must be in the form "view:attribute". The value must be of a type that the provider * for the view accepts. * *

For the included attribute views, default values can be set for the following attributes: * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
AttributeLegal Types
{@code "owner:owner"}{@code String} (user name)
{@code "posix:group"}{@code String} (group name)
{@code "posix:permissions"}{@code String} (format "rwxrw-r--"), {@code Set}
{@code "dos:readonly"}{@code Boolean}
{@code "dos:hidden"}{@code Boolean}
{@code "dos:archive"}{@code Boolean}
{@code "dos:system"}{@code Boolean}
{@code "acl:acl"}{@code List}
*/ public Builder setDefaultAttributeValue(String attribute, Object value) { checkArgument( ATTRIBUTE_PATTERN.matcher(attribute).matches(), "attribute (%s) must be of the form \"view:attribute\"", attribute); checkNotNull(value); if (defaultAttributeValues == null) { defaultAttributeValues = new HashMap<>(); } defaultAttributeValues.put(attribute, value); return this; } private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile("[^:]+:[^:]+"); /** * Sets the roots for the file system. * * @throws InvalidPathException if any of the given roots is not a valid path for this builder's * path type * @throws IllegalArgumentException if any of the given roots is a valid path for this builder's * path type but is not a root path with no name elements */ public Builder setRoots(String first, String... more) { List roots = Lists.asList(first, more); for (String root : roots) { PathType.ParseResult parseResult = pathType.parsePath(root); checkArgument(parseResult.isRoot(), "invalid root: %s", root); } this.roots = ImmutableSet.copyOf(roots); return this; } /** * Sets the path to the working directory for the file system. The working directory must be an * absolute path starting with one of the configured roots. * * @throws InvalidPathException if the given path is not valid for this builder's path type * @throws IllegalArgumentException if the given path is valid for this builder's path type but * is not an absolute path */ public Builder setWorkingDirectory(String workingDirectory) { PathType.ParseResult parseResult = pathType.parsePath(workingDirectory); checkArgument( parseResult.isAbsolute(), "working directory must be an absolute path: %s", workingDirectory); this.workingDirectory = checkNotNull(workingDirectory); return this; } /** * Sets the given features to be supported by the file system. Any features not provided here * will not be supported. */ public Builder setSupportedFeatures(Feature... features) { supportedFeatures = Sets.immutableEnumSet(Arrays.asList(features)); return this; } /** * Sets the configuration that {@link WatchService} instances created by the file system should * use. The default configuration polls watched directories for changes every 5 seconds. * * @since 1.1 */ public Builder setWatchServiceConfiguration(WatchServiceConfiguration config) { this.watchServiceConfig = checkNotNull(config); return this; } private Builder setDisplayName(String displayName) { this.displayName = checkNotNull(displayName); return this; } /** Creates a new immutable configuration object from this builder. */ public Configuration build() { return new Configuration(this); } } }