| /* |
| * 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.apache.hadoop.fs.s3a.s3guard; |
| |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| |
| import com.google.common.base.Preconditions; |
| |
| import org.apache.hadoop.classification.InterfaceAudience; |
| import org.apache.hadoop.classification.InterfaceStability; |
| import org.apache.hadoop.fs.FileStatus; |
| import org.apache.hadoop.fs.Path; |
| import org.apache.hadoop.fs.s3a.Tristate; |
| |
| /** |
| * {@code DirListingMetadata} models a directory listing stored in a |
| * {@link MetadataStore}. Instances of this class are mutable and thread-safe. |
| */ |
| @InterfaceAudience.Private |
| @InterfaceStability.Evolving |
| public class DirListingMetadata { |
| |
| /** |
| * Convenience parameter for passing into constructor. |
| */ |
| public static final Collection<PathMetadata> EMPTY_DIR = |
| Collections.emptyList(); |
| |
| private final Path path; |
| |
| /** Using a map for fast find / remove with large directories. */ |
| private Map<Path, PathMetadata> listMap = new ConcurrentHashMap<>(); |
| |
| private boolean isAuthoritative; |
| |
| /** |
| * Create a directory listing metadata container. |
| * |
| * @param path Path of the directory. If this path has a host component, then |
| * all paths added later via {@link #put(FileStatus)} must also have |
| * the same host. |
| * @param listing Entries in the directory. |
| * @param isAuthoritative true iff listing is the full contents of the |
| * directory, and the calling client reports that this may be cached as |
| * the full and authoritative listing of all files in the directory. |
| */ |
| public DirListingMetadata(Path path, Collection<PathMetadata> listing, |
| boolean isAuthoritative) { |
| |
| checkPathAbsolute(path); |
| this.path = path; |
| |
| if (listing != null) { |
| for (PathMetadata entry : listing) { |
| Path childPath = entry.getFileStatus().getPath(); |
| checkChildPath(childPath); |
| listMap.put(childPath, entry); |
| } |
| } |
| this.isAuthoritative = isAuthoritative; |
| } |
| |
| /** |
| * Copy constructor. |
| * @param d the existing {@link DirListingMetadata} object. |
| */ |
| public DirListingMetadata(DirListingMetadata d) { |
| path = d.path; |
| isAuthoritative = d.isAuthoritative; |
| listMap = new ConcurrentHashMap<>(d.listMap); |
| } |
| |
| /** |
| * @return {@code Path} of the directory that contains this listing. |
| */ |
| public Path getPath() { |
| return path; |
| } |
| |
| /** |
| * @return entries in the directory |
| */ |
| public Collection<PathMetadata> getListing() { |
| return Collections.unmodifiableCollection(listMap.values()); |
| } |
| |
| public Set<Path> listTombstones() { |
| Set<Path> tombstones = new HashSet<>(); |
| for (PathMetadata meta : listMap.values()) { |
| if (meta.isDeleted()) { |
| tombstones.add(meta.getFileStatus().getPath()); |
| } |
| } |
| return tombstones; |
| } |
| |
| public DirListingMetadata withoutTombstones() { |
| Collection<PathMetadata> filteredList = new ArrayList<>(); |
| for (PathMetadata meta : listMap.values()) { |
| if (!meta.isDeleted()) { |
| filteredList.add(meta); |
| } |
| } |
| return new DirListingMetadata(path, filteredList, isAuthoritative); |
| } |
| |
| /** |
| * @return number of entries tracked. This is not the same as the number |
| * of entries in the actual directory unless {@link #isAuthoritative()} is |
| * true. |
| */ |
| public int numEntries() { |
| return listMap.size(); |
| } |
| |
| /** |
| * @return true iff this directory listing is full and authoritative within |
| * the scope of the {@code MetadataStore} that returned it. |
| */ |
| public boolean isAuthoritative() { |
| return isAuthoritative; |
| } |
| |
| |
| /** |
| * Is the underlying directory known to be empty? |
| * @return FALSE if directory is known to have a child entry, TRUE if |
| * directory is known to be empty, UNKNOWN otherwise. |
| */ |
| public Tristate isEmpty() { |
| if (getListing().isEmpty()) { |
| if (isAuthoritative()) { |
| return Tristate.TRUE; |
| } else { |
| // This listing is empty, but may not be full list of underlying dir. |
| return Tristate.UNKNOWN; |
| } |
| } else { // not empty listing |
| // There exists at least one child, dir not empty. |
| return Tristate.FALSE; |
| } |
| } |
| |
| /** |
| * Marks this directory listing as full and authoritative. |
| * @param authoritative see {@link #isAuthoritative()}. |
| */ |
| public void setAuthoritative(boolean authoritative) { |
| this.isAuthoritative = authoritative; |
| } |
| |
| /** |
| * Lookup entry within this directory listing. This may return null if the |
| * {@code MetadataStore} only tracks a partial set of the directory entries. |
| * In the case where {@link #isAuthoritative()} is true, however, this |
| * function returns null iff the directory is known not to contain the listing |
| * at given path (within the scope of the {@code MetadataStore} that returned |
| * it). |
| * |
| * @param childPath path of entry to look for. |
| * @return entry, or null if it is not present or not being tracked. |
| */ |
| public PathMetadata get(Path childPath) { |
| checkChildPath(childPath); |
| return listMap.get(childPath); |
| } |
| |
| /** |
| * Replace an entry with a tombstone. |
| * @param childPath path of entry to replace. |
| */ |
| public void markDeleted(Path childPath) { |
| checkChildPath(childPath); |
| listMap.put(childPath, PathMetadata.tombstone(childPath)); |
| } |
| |
| /** |
| * Remove entry from this directory. |
| * |
| * @param childPath path of entry to remove. |
| */ |
| public void remove(Path childPath) { |
| checkChildPath(childPath); |
| listMap.remove(childPath); |
| } |
| |
| /** |
| * Add an entry to the directory listing. If this listing already contains a |
| * {@code FileStatus} with the same path, it will be replaced. |
| * |
| * @param childFileStatus entry to add to this directory listing. |
| * @return true if the status was added or replaced with a new value. False |
| * if the same FileStatus value was already present. |
| */ |
| public boolean put(FileStatus childFileStatus) { |
| Preconditions.checkNotNull(childFileStatus, |
| "childFileStatus must be non-null"); |
| Path childPath = childStatusToPathKey(childFileStatus); |
| PathMetadata newValue = new PathMetadata(childFileStatus); |
| PathMetadata oldValue = listMap.put(childPath, newValue); |
| return oldValue == null || !oldValue.equals(newValue); |
| } |
| |
| @Override |
| public String toString() { |
| return "DirListingMetadata{" + |
| "path=" + path + |
| ", listMap=" + listMap + |
| ", isAuthoritative=" + isAuthoritative + |
| '}'; |
| } |
| |
| /** |
| * Log contents to supplied StringBuilder in a pretty fashion. |
| * @param sb target StringBuilder |
| */ |
| public void prettyPrint(StringBuilder sb) { |
| sb.append(String.format("DirMeta %-20s %-18s", |
| path.toString(), |
| isAuthoritative ? "Authoritative" : "Not Authoritative")); |
| for (Map.Entry<Path, PathMetadata> entry : listMap.entrySet()) { |
| sb.append("\n key: ").append(entry.getKey()).append(": "); |
| entry.getValue().prettyPrint(sb); |
| } |
| sb.append("\n"); |
| } |
| |
| public String prettyPrint() { |
| StringBuilder sb = new StringBuilder(); |
| prettyPrint(sb); |
| return sb.toString(); |
| } |
| |
| /** |
| * Checks that child path is valid. |
| * @param childPath path to check. |
| */ |
| private void checkChildPath(Path childPath) { |
| checkPathAbsolute(childPath); |
| |
| // If this dir's path has host (and thus scheme), so must its children |
| URI parentUri = path.toUri(); |
| if (parentUri.getHost() != null) { |
| URI childUri = childPath.toUri(); |
| Preconditions.checkNotNull(childUri.getHost(), "Expected non-null URI " + |
| "host: %s", childUri); |
| Preconditions.checkArgument( |
| childUri.getHost().equals(parentUri.getHost()), |
| "childUri %s and parentUri %s must have the same host", |
| childUri, parentUri); |
| Preconditions.checkNotNull(childUri.getScheme(), "No scheme in path %s", |
| childUri); |
| } |
| Preconditions.checkArgument(!childPath.isRoot(), |
| "childPath cannot be the root path: %s", childPath); |
| Preconditions.checkArgument(childPath.getParent().equals(path), |
| "childPath %s must be a child of %s", childPath, path); |
| } |
| |
| /** |
| * For Paths that are handed in directly, we assert they are in consistent |
| * format with checkPath(). For paths that are supplied embedded in |
| * FileStatus, we attempt to fill in missing scheme and host, when this |
| * DirListingMetadata is associated with one. |
| * |
| * @return Path suitable for consistent hashtable lookups |
| * @throws NullPointerException null status argument |
| * @throws IllegalArgumentException bad status values or failure to |
| * create a URI. |
| */ |
| private Path childStatusToPathKey(FileStatus status) { |
| Path p = status.getPath(); |
| Preconditions.checkNotNull(p, "Child status' path cannot be null"); |
| Preconditions.checkArgument(!p.isRoot(), |
| "childPath cannot be the root path: %s", p); |
| Preconditions.checkArgument(p.getParent().equals(path), |
| "childPath %s must be a child of %s", p, path); |
| URI uri = p.toUri(); |
| URI parentUri = path.toUri(); |
| // If FileStatus' path is missing host, but should have one, add it. |
| if (uri.getHost() == null && parentUri.getHost() != null) { |
| try { |
| return new Path(new URI(parentUri.getScheme(), parentUri.getHost(), |
| uri.getPath(), uri.getFragment())); |
| } catch (URISyntaxException e) { |
| throw new IllegalArgumentException("FileStatus path invalid with" + |
| " added " + parentUri.getScheme() + "://" + parentUri.getHost() + |
| " added", e); |
| } |
| } |
| return p; |
| } |
| |
| private void checkPathAbsolute(Path p) { |
| Preconditions.checkNotNull(p, "path must be non-null"); |
| Preconditions.checkArgument(p.isAbsolute(), "path must be absolute: %s", p); |
| } |
| } |