001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.io.file;
019
020import java.io.IOException;
021import java.math.BigInteger;
022import java.nio.file.FileVisitResult;
023import java.nio.file.Files;
024import java.nio.file.Path;
025import java.nio.file.attribute.BasicFileAttributes;
026import java.util.Objects;
027import java.util.function.UnaryOperator;
028
029import org.apache.commons.io.file.Counters.PathCounters;
030import org.apache.commons.io.filefilter.IOFileFilter;
031import org.apache.commons.io.filefilter.SymbolicLinkFileFilter;
032import org.apache.commons.io.filefilter.TrueFileFilter;
033import org.apache.commons.io.function.IOBiFunction;
034
035/**
036 * Counts files, directories, and sizes, as a visit proceeds.
037 *
038 * @since 2.7
039 */
040public class CountingPathVisitor extends SimplePathVisitor {
041
042    /**
043     * Builds instances of {@link CountingPathVisitor}.
044     *
045     * @param <T> The CountingPathVisitor type.
046     * @param <B> The AbstractBuilder type.
047     * @since 2.19.0
048     */
049    public abstract static class AbstractBuilder<T, B extends AbstractBuilder<T, B>> extends SimplePathVisitor.AbstractBuilder<T, B> {
050
051        private PathCounters pathCounters = defaultPathCounters();
052        private PathFilter fileFilter = defaultFileFilter();
053        private PathFilter directoryFilter = defaultDirectoryFilter();
054        private UnaryOperator<Path> directoryPostTransformer = defaultDirectoryTransformer();
055
056        /**
057         * Constructs a new builder for subclasses.
058         */
059        public AbstractBuilder() {
060            // empty.
061        }
062
063        PathFilter getDirectoryFilter() {
064            return directoryFilter;
065        }
066
067        UnaryOperator<Path> getDirectoryPostTransformer() {
068            return directoryPostTransformer;
069        }
070
071        PathFilter getFileFilter() {
072            return fileFilter;
073        }
074
075        PathCounters getPathCounters() {
076            return pathCounters;
077        }
078
079        /**
080         * Sets how to filter directories.
081         *
082         * @param directoryFilter how to filter files.
083         * @return this instance.
084         */
085        public B setDirectoryFilter(final PathFilter directoryFilter) {
086            this.directoryFilter = directoryFilter != null ? directoryFilter : defaultDirectoryFilter();
087            return asThis();
088        }
089
090        /**
091         * Sets how to transform directories, defaults to {@link UnaryOperator#identity()}.
092         *
093         * @param directoryTransformer how to filter files.
094         * @return this instance.
095         */
096        public B setDirectoryPostTransformer(final UnaryOperator<Path> directoryTransformer) {
097            this.directoryPostTransformer = directoryTransformer != null ? directoryTransformer : defaultDirectoryTransformer();
098            return asThis();
099        }
100
101        /**
102         * Sets how to filter files.
103         *
104         * @param fileFilter how to filter files.
105         * @return this instance.
106         */
107        public B setFileFilter(final PathFilter fileFilter) {
108            this.fileFilter = fileFilter != null ? fileFilter : defaultFileFilter();
109            return asThis();
110        }
111
112        /**
113         * Sets how to count path visits.
114         *
115         * @param pathCounters How to count path visits.
116         * @return this instance.
117         */
118        public B setPathCounters(final PathCounters pathCounters) {
119            this.pathCounters = pathCounters != null ? pathCounters : defaultPathCounters();
120            return asThis();
121        }
122    }
123
124    /**
125     * Builds instances of {@link CountingPathVisitor}.
126     *
127     * @since 2.18.0
128     */
129    public static class Builder extends AbstractBuilder<CountingPathVisitor, Builder> {
130
131        /**
132         * Constructs a new builder.
133         */
134        public Builder() {
135            // empty.
136        }
137
138        @Override
139        public CountingPathVisitor get() {
140            return new CountingPathVisitor(this);
141        }
142    }
143
144    static final String[] EMPTY_STRING_ARRAY = {};
145
146    static IOFileFilter defaultDirectoryFilter() {
147        return TrueFileFilter.INSTANCE;
148    }
149
150    static UnaryOperator<Path> defaultDirectoryTransformer() {
151        return UnaryOperator.identity();
152    }
153
154    static IOFileFilter defaultFileFilter() {
155        return new SymbolicLinkFileFilter(FileVisitResult.TERMINATE, FileVisitResult.CONTINUE);
156    }
157
158    static PathCounters defaultPathCounters() {
159        return Counters.longPathCounters();
160    }
161
162    /**
163     * Constructs a new instance configured with a {@link BigInteger} {@link PathCounters}.
164     *
165     * @return a new instance configured with a {@link BigInteger} {@link PathCounters}.
166     */
167    public static CountingPathVisitor withBigIntegerCounters() {
168        return new Builder().setPathCounters(Counters.bigIntegerPathCounters()).get();
169    }
170
171    /**
172     * Constructs a new instance configured with a {@code long} {@link PathCounters}.
173     *
174     * @return a new instance configured with a {@code long} {@link PathCounters}.
175     */
176    public static CountingPathVisitor withLongCounters() {
177        return new Builder().setPathCounters(Counters.longPathCounters()).get();
178    }
179
180    private final PathCounters pathCounters;
181    private final PathFilter fileFilter;
182    private final PathFilter directoryFilter;
183    private final UnaryOperator<Path> directoryPostTransformer;
184
185    CountingPathVisitor(final AbstractBuilder<?, ?> builder) {
186        super(builder);
187        this.pathCounters = builder.getPathCounters();
188        this.fileFilter = builder.getFileFilter();
189        this.directoryFilter = builder.getDirectoryFilter();
190        this.directoryPostTransformer = builder.getDirectoryPostTransformer();
191    }
192
193    /**
194     * Constructs a new instance.
195     *
196     * @param pathCounters How to count path visits.
197     * @see Builder
198     */
199    public CountingPathVisitor(final PathCounters pathCounters) {
200        this(new Builder().setPathCounters(pathCounters));
201    }
202
203    /**
204     * Constructs a new instance.
205     *
206     * @param pathCounters    How to count path visits.
207     * @param fileFilter      Filters which files to count.
208     * @param directoryFilter Filters which directories to count.
209     * @see Builder
210     * @since 2.9.0
211     */
212    public CountingPathVisitor(final PathCounters pathCounters, final PathFilter fileFilter, final PathFilter directoryFilter) {
213        this.pathCounters = Objects.requireNonNull(pathCounters, "pathCounters");
214        this.fileFilter = Objects.requireNonNull(fileFilter, "fileFilter");
215        this.directoryFilter = Objects.requireNonNull(directoryFilter, "directoryFilter");
216        this.directoryPostTransformer = UnaryOperator.identity();
217    }
218
219    /**
220     * Constructs a new instance.
221     *
222     * @param pathCounters    How to count path visits.
223     * @param fileFilter      Filters which files to count.
224     * @param directoryFilter Filters which directories to count.
225     * @param visitFileFailed Called on {@link #visitFileFailed(Path, IOException)}.
226     * @since 2.12.0
227     * @deprecated Use {@link Builder}.
228     */
229    @Deprecated
230    public CountingPathVisitor(final PathCounters pathCounters, final PathFilter fileFilter, final PathFilter directoryFilter,
231            final IOBiFunction<Path, IOException, FileVisitResult> visitFileFailed) {
232        super(visitFileFailed);
233        this.pathCounters = Objects.requireNonNull(pathCounters, "pathCounters");
234        this.fileFilter = Objects.requireNonNull(fileFilter, "fileFilter");
235        this.directoryFilter = Objects.requireNonNull(directoryFilter, "directoryFilter");
236        this.directoryPostTransformer = UnaryOperator.identity();
237    }
238
239    @Override
240    public boolean equals(final Object obj) {
241        if (this == obj) {
242            return true;
243        }
244        if (!(obj instanceof CountingPathVisitor)) {
245            return false;
246        }
247        final CountingPathVisitor other = (CountingPathVisitor) obj;
248        return Objects.equals(pathCounters, other.pathCounters);
249    }
250
251    /**
252     * Gets the visitation counts.
253     *
254     * @return the visitation counts.
255     */
256    public PathCounters getPathCounters() {
257        return pathCounters;
258    }
259
260    @Override
261    public int hashCode() {
262        return Objects.hash(pathCounters);
263    }
264
265    @Override
266    public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) throws IOException {
267        updateDirCounter(directoryPostTransformer.apply(dir), exc);
268        return FileVisitResult.CONTINUE;
269    }
270
271    @Override
272    public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attributes) throws IOException {
273        final FileVisitResult accept = directoryFilter.accept(dir, attributes);
274        return accept != FileVisitResult.CONTINUE ? FileVisitResult.SKIP_SUBTREE : FileVisitResult.CONTINUE;
275    }
276
277    @Override
278    public String toString() {
279        return pathCounters.toString();
280    }
281
282    /**
283     * Updates the counter for visiting the given directory.
284     *
285     * @param dir the visited directory.
286     * @param exc Encountered exception.
287     * @since 2.9.0
288     */
289    protected void updateDirCounter(final Path dir, final IOException exc) {
290        pathCounters.getDirectoryCounter().increment();
291    }
292
293    /**
294     * Updates the counters for visiting the given file.
295     *
296     * @param file       the visited file.
297     * @param attributes the visited file attributes.
298     */
299    protected void updateFileCounters(final Path file, final BasicFileAttributes attributes) {
300        pathCounters.getFileCounter().increment();
301        pathCounters.getByteCounter().add(attributes.size());
302    }
303
304    @Override
305    public FileVisitResult visitFile(final Path file, final BasicFileAttributes attributes) throws IOException {
306        // Note: A file can be a symbolic link to a directory.
307        if (Files.exists(file) && fileFilter.accept(file, attributes) == FileVisitResult.CONTINUE) {
308            updateFileCounters(file, attributes);
309        }
310        return FileVisitResult.CONTINUE;
311    }
312}