Configuration.java
/*
* 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 com.google.errorprone.annotations.CanIgnoreReturnValue;
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.qual.Nullable;
/**
* 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:
*
* <ul>
* <li>uses {@code /} as the path name separator (see {@link PathType#unix()} for more
* information on the path format)
* <li>has root {@code /} and working directory {@code /work}
* <li>performs case-sensitive file lookup
* <li>supports only the {@linkplain BasicFileAttributeView basic} file attribute view, to avoid
* overhead for unneeded attributes
* <li>supports hard links, symbolic links, {@link SecureDirectoryStream} and {@link
* FileChannel}
* </ul>
*
* <p>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}.
*
* <p>Example:
*
* <pre>
* Configuration config = Configuration.unix().toBuilder()
* .setAttributeViews("basic", "owner", "posix", "unix")
* .setWorkingDirectory("/home/user")
* .build(); </pre>
*/
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.
*
* <p>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.
*
* <p>A file system created with this configuration:
*
* <ul>
* <li>uses {@code /} as the path name separator (see {@link PathType#unix()} for more
* information on the path format)
* <li>has root {@code /} and working directory {@code /work}
* <li>does Unicode normalization on paths, both for lookup and for {@code Path} objects
* <li>does case-insensitive (for ASCII characters only) lookup
* <li>supports only the {@linkplain BasicFileAttributeView basic} file attribute view, to avoid
* overhead for unneeded attributes
* <li>supports hard links, symbolic links and {@link FileChannel}
* </ul>
*
* <p>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}.
*
* <p>Example:
*
* <pre>
* Configuration config = Configuration.osX().toBuilder()
* .setAttributeViews("basic", "owner", "posix", "unix")
* .setNameCanonicalNormalization(NFD, CASE_FOLD_UNICODE)
* .setWorkingDirectory("/Users/user")
* .build(); </pre>
*/
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:
*
* <ul>
* <li>uses {@code \} as the path name separator and recognizes {@code /} as a separator when
* parsing paths (see {@link PathType#windows()} for more information on path format)
* <li>has root {@code C:\} and working directory {@code C:\work}
* <li>performs case-insensitive (for ASCII characters only) file lookup
* <li>creates {@code Path} objects that use case-insensitive (for ASCII characters only)
* equality
* <li>supports only the {@linkplain BasicFileAttributeView basic} file attribute view, to avoid
* overhead for unneeded attributes
* <li>supports hard links, symbolic links and {@link FileChannel}
* </ul>
*
* <p>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}.
*
* <p>Example:
*
* <pre>
* Configuration config = Configuration.windows().toBuilder()
* .setAttributeViews("basic", "owner", "dos", "acl", "user")
* .setNameCanonicalNormalization(CASE_FOLD_UNICODE)
* .setWorkingDirectory("C:\\Users\\user") // or "C:/Users/user"
* .build(); </pre>
*/
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.
*
* <p>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.
*
* <p>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<PathNormalization> nameDisplayNormalization;
final ImmutableSet<PathNormalization> nameCanonicalNormalization;
final boolean pathEqualityUsesCanonicalForm;
// Disk configuration
final int blockSize;
final long maxSize;
final long maxCacheSize;
// Attribute configuration
final ImmutableSet<String> attributeViews;
final ImmutableSet<AttributeProvider> attributeProviders;
final ImmutableMap<String, Object> defaultAttributeValues;
final FileTimeSource fileTimeSource;
// Watch service
final WatchServiceConfiguration watchServiceConfig;
// Other
final ImmutableSet<String> roots;
final String workingDirectory;
final ImmutableSet<Feature> 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.<AttributeProvider>of()
: ImmutableSet.copyOf(builder.attributeProviders);
this.defaultAttributeValues =
builder.defaultAttributeValues == null
? ImmutableMap.<String, Object>of()
: ImmutableMap.copyOf(builder.defaultAttributeValues);
this.fileTimeSource = builder.fileTimeSource;
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);
}
helper.add("fileTimeSource", fileTimeSource);
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<PathNormalization> nameDisplayNormalization = ImmutableSet.of();
private ImmutableSet<PathNormalization> 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<String> attributeViews = ImmutableSet.of();
private Set<AttributeProvider> attributeProviders = null;
private Map<String, Object> defaultAttributeValues;
private FileTimeSource fileTimeSource = SystemFileTimeSource.INSTANCE;
// Watch service
private WatchServiceConfiguration watchServiceConfig = WatchServiceConfiguration.DEFAULT;
// Other
private ImmutableSet<String> roots = ImmutableSet.of();
private String workingDirectory;
private ImmutableSet<Feature> 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.fileTimeSource = configuration.fileTimeSource;
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.
*/
@CanIgnoreReturnValue
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.
*/
@CanIgnoreReturnValue
public Builder setNameCanonicalNormalization(
PathNormalization first, PathNormalization... more) {
this.nameCanonicalNormalization = checkNormalizations(Lists.asList(first, more));
return this;
}
private ImmutableSet<PathNormalization> checkNormalizations(
List<PathNormalization> 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, @Nullable 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.
*
* <p>The default is false.
*/
@CanIgnoreReturnValue
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.
*
* <p>The default is 8192 bytes (8 KB).
*/
@CanIgnoreReturnValue
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.
*
* <p><b>Note:</b> 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.
*
* <p>The default is 4 GB.
*/
@CanIgnoreReturnValue
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.
*
* <p>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.
*/
@CanIgnoreReturnValue
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:
*
* <table>
* <tr>
* <td><b>Name</b></td>
* <td><b>View Interface</b></td>
* <td><b>Attributes Interface</b></td>
* </tr>
* <tr>
* <td>{@code "basic"}</td>
* <td>{@link java.nio.file.attribute.BasicFileAttributeView BasicFileAttributeView}</td>
* <td>{@link java.nio.file.attribute.BasicFileAttributes BasicFileAttributes}</td>
* </tr>
* <tr>
* <td>{@code "owner"}</td>
* <td>{@link java.nio.file.attribute.FileOwnerAttributeView FileOwnerAttributeView}</td>
* <td>--</td>
* </tr>
* <tr>
* <td>{@code "posix"}</td>
* <td>{@link java.nio.file.attribute.PosixFileAttributeView PosixFileAttributeView}</td>
* <td>{@link java.nio.file.attribute.PosixFileAttributes PosixFileAttributes}</td>
* </tr>
* <tr>
* <td>{@code "unix"}</td>
* <td>--</td>
* <td>--</td>
* </tr>
* <tr>
* <td>{@code "dos"}</td>
* <td>{@link java.nio.file.attribute.DosFileAttributeView DosFileAttributeView}</td>
* <td>{@link java.nio.file.attribute.DosFileAttributes DosFileAttributes}</td>
* </tr>
* <tr>
* <td>{@code "acl"}</td>
* <td>{@link java.nio.file.attribute.AclFileAttributeView AclFileAttributeView}</td>
* <td>--</td>
* </tr>
* <tr>
* <td>{@code "user"}</td>
* <td>{@link java.nio.file.attribute.UserDefinedFileAttributeView UserDefinedFileAttributeView}</td>
* <td>--</td>
* </tr>
* </table>
*
* <p>If any other views should be supported, attribute providers for those views must be
* {@linkplain #addAttributeProvider(AttributeProvider) added}.
*/
@CanIgnoreReturnValue
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. */
@CanIgnoreReturnValue
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.
*
* <p>For the included attribute views, default values can be set for the following attributes:
*
* <table>
* <tr>
* <th>Attribute</th>
* <th>Legal Types</th>
* </tr>
* <tr>
* <td>{@code "owner:owner"}</td>
* <td>{@code String} (user name)</td>
* </tr>
* <tr>
* <td>{@code "posix:group"}</td>
* <td>{@code String} (group name)</td>
* </tr>
* <tr>
* <td>{@code "posix:permissions"}</td>
* <td>{@code String} (format "rwxrw-r--"), {@code Set<PosixFilePermission>}</td>
* </tr>
* <tr>
* <td>{@code "dos:readonly"}</td>
* <td>{@code Boolean}</td>
* </tr>
* <tr>
* <td>{@code "dos:hidden"}</td>
* <td>{@code Boolean}</td>
* </tr>
* <tr>
* <td>{@code "dos:archive"}</td>
* <td>{@code Boolean}</td>
* </tr>
* <tr>
* <td>{@code "dos:system"}</td>
* <td>{@code Boolean}</td>
* </tr>
* <tr>
* <td>{@code "acl:acl"}</td>
* <td>{@code List<AclEntry>}</td>
* </tr>
* </table>
*/
@CanIgnoreReturnValue
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 {@link FileTimeSource} that will supply the current time for this file system.
*
* @since 1.3
*/
@CanIgnoreReturnValue
public Builder setFileTimeSource(FileTimeSource source) {
this.fileTimeSource = checkNotNull(source);
return this;
}
/**
* 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
*/
@CanIgnoreReturnValue
public Builder setRoots(String first, String... more) {
List<String> 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
*/
@CanIgnoreReturnValue
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.
*/
@CanIgnoreReturnValue
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
*/
@CanIgnoreReturnValue
public Builder setWatchServiceConfiguration(WatchServiceConfiguration config) {
this.watchServiceConfig = checkNotNull(config);
return this;
}
@CanIgnoreReturnValue
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);
}
}
}