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 *      https://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.TrueFileFilter;
032import org.apache.commons.io.function.IOBiFunction;
033
034/**
035 * Counts files, directories, and sizes, as a visit proceeds.
036 *
037 * @since 2.7
038 */
039public class CountingPathVisitor extends SimplePathVisitor {
040
041    /**
042     * Builds instances of {@link CountingPathVisitor}.
043     *
044     * @param <T> The CountingPathVisitor type.
045     * @param <B> The AbstractBuilder type.
046     * @since 2.19.0
047     */
048    public abstract static class AbstractBuilder<T, B extends AbstractBuilder<T, B>> extends SimplePathVisitor.AbstractBuilder<T, B> {
049
050        private PathCounters pathCounters = defaultPathCounters();
051        private PathFilter fileFilter = defaultFileFilter();
052        private PathFilter directoryFilter = defaultDirectoryFilter();
053        private UnaryOperator<Path> directoryPostTransformer = defaultDirectoryTransformer();
054
055        /**
056         * Constructs a new builder for subclasses.
057         */
058        public AbstractBuilder() {
059            // empty.
060        }
061
062        PathFilter getDirectoryFilter() {
063            return directoryFilter;
064        }
065
066        UnaryOperator<Path> getDirectoryPostTransformer() {
067            return directoryPostTransformer;
068        }
069
070        PathFilter getFileFilter() {
071            return fileFilter;
072        }
073
074        PathCounters getPathCounters() {
075            return pathCounters;
076        }
077
078        /**
079         * Sets how to filter directories.
080         *
081         * @param directoryFilter how to filter files.
082         * @return {@code this} instance.
083         */
084        public B setDirectoryFilter(final PathFilter directoryFilter) {
085            this.directoryFilter = directoryFilter != null ? directoryFilter : defaultDirectoryFilter();
086            return asThis();
087        }
088
089        /**
090         * Sets how to transform directories, defaults to {@link UnaryOperator#identity()}.
091         *
092         * @param directoryTransformer how to filter files.
093         * @return {@code this} instance.
094         */
095        public B setDirectoryPostTransformer(final UnaryOperator<Path> directoryTransformer) {
096            this.directoryPostTransformer = directoryTransformer != null ? directoryTransformer : defaultDirectoryTransformer();
097            return asThis();
098        }
099
100        /**
101         * Sets how to filter files.
102         *
103         * @param fileFilter how to filter files.
104         * @return {@code this} instance.
105         */
106        public B setFileFilter(final PathFilter fileFilter) {
107            this.fileFilter = fileFilter != null ? fileFilter : defaultFileFilter();
108            return asThis();
109        }
110
111        /**
112         * Sets how to count path visits.
113         *
114         * @param pathCounters How to count path visits.
115         * @return {@code this} instance.
116         */
117        public B setPathCounters(final PathCounters pathCounters) {
118            this.pathCounters = pathCounters != null ? pathCounters : defaultPathCounters();
119            return asThis();
120        }
121    }
122
123    /**
124     * Builds instances of {@link CountingPathVisitor}.
125     *
126     * @since 2.18.0
127     */
128    public static class Builder extends AbstractBuilder<CountingPathVisitor, Builder> {
129
130        /**
131         * Constructs a new builder.
132         */
133        public Builder() {
134            // empty.
135        }
136
137        @Override
138        public CountingPathVisitor get() {
139            return new CountingPathVisitor(this);
140        }
141    }
142
143    static final String[] EMPTY_STRING_ARRAY = {};
144
145    static IOFileFilter defaultDirectoryFilter() {
146        return TrueFileFilter.INSTANCE;
147    }
148
149    static UnaryOperator<Path> defaultDirectoryTransformer() {
150        return UnaryOperator.identity();
151    }
152
153    static IOFileFilter defaultFileFilter() {
154        return TrueFileFilter.INSTANCE;
155    }
156
157    static PathCounters defaultPathCounters() {
158        return Counters.longPathCounters();
159    }
160
161    /**
162     * Constructs a new instance configured with a {@link BigInteger} {@link PathCounters}.
163     *
164     * @return a new instance configured with a {@link BigInteger} {@link PathCounters}.
165     */
166    public static CountingPathVisitor withBigIntegerCounters() {
167        return new Builder().setPathCounters(Counters.bigIntegerPathCounters()).get();
168    }
169
170    /**
171     * Constructs a new instance configured with a {@code long} {@link PathCounters}.
172     *
173     * @return a new instance configured with a {@code long} {@link PathCounters}.
174     */
175    public static CountingPathVisitor withLongCounters() {
176        return new Builder().setPathCounters(Counters.longPathCounters()).get();
177    }
178
179    private final PathCounters pathCounters;
180    private final PathFilter fileFilter;
181    private final PathFilter directoryFilter;
182    private final UnaryOperator<Path> directoryPostTransformer;
183
184    CountingPathVisitor(final AbstractBuilder<?, ?> builder) {
185        super(builder);
186        this.pathCounters = builder.getPathCounters();
187        this.fileFilter = builder.getFileFilter();
188        this.directoryFilter = builder.getDirectoryFilter();
189        this.directoryPostTransformer = builder.getDirectoryPostTransformer();
190    }
191
192    /**
193     * Constructs a new instance.
194     *
195     * @param pathCounters How to count path visits.
196     * @see Builder
197     */
198    public CountingPathVisitor(final PathCounters pathCounters) {
199        this(new Builder().setPathCounters(pathCounters));
200    }
201
202    /**
203     * Constructs a new instance.
204     *
205     * @param pathCounters    How to count path visits.
206     * @param fileFilter      Filters which files to count.
207     * @param directoryFilter Filters which directories to count.
208     * @see Builder
209     * @since 2.9.0
210     */
211    public CountingPathVisitor(final PathCounters pathCounters, final PathFilter fileFilter, final PathFilter directoryFilter) {
212        this.pathCounters = Objects.requireNonNull(pathCounters, "pathCounters");
213        this.fileFilter = Objects.requireNonNull(fileFilter, "fileFilter");
214        this.directoryFilter = Objects.requireNonNull(directoryFilter, "directoryFilter");
215        this.directoryPostTransformer = UnaryOperator.identity();
216    }
217
218    /**
219     * Constructs a new instance.
220     *
221     * @param pathCounters    How to count path visits.
222     * @param fileFilter      Filters which files to count.
223     * @param directoryFilter Filters which directories to count.
224     * @param visitFileFailed Called on {@link #visitFileFailed(Path, IOException)}.
225     * @since 2.12.0
226     * @deprecated Use {@link Builder}.
227     */
228    @Deprecated
229    public CountingPathVisitor(final PathCounters pathCounters, final PathFilter fileFilter, final PathFilter directoryFilter,
230            final IOBiFunction<Path, IOException, FileVisitResult> visitFileFailed) {
231        super(visitFileFailed);
232        this.pathCounters = Objects.requireNonNull(pathCounters, "pathCounters");
233        this.fileFilter = Objects.requireNonNull(fileFilter, "fileFilter");
234        this.directoryFilter = Objects.requireNonNull(directoryFilter, "directoryFilter");
235        this.directoryPostTransformer = UnaryOperator.identity();
236    }
237
238    /**
239     * Tests whether the given file is accepted by the file filter.
240     *
241     * @param file       the visited file.
242     * @param attributes the visited file attributes.
243     * @return true to copy the given file, false if not.
244     * @since 2.20.0
245     */
246    protected boolean accept(final Path file, final BasicFileAttributes attributes) {
247        // Note: A file can be a symbolic link to a directory.
248        return Files.exists(file) && fileFilter.accept(file, attributes) == FileVisitResult.CONTINUE;
249    }
250
251    @Override
252    public boolean equals(final Object obj) {
253        if (this == obj) {
254            return true;
255        }
256        if (!(obj instanceof CountingPathVisitor)) {
257            return false;
258        }
259        final CountingPathVisitor other = (CountingPathVisitor) obj;
260        return Objects.equals(pathCounters, other.pathCounters);
261    }
262
263    /**
264     * Gets the visitation counts.
265     *
266     * @return the visitation counts.
267     */
268    public PathCounters getPathCounters() {
269        return pathCounters;
270    }
271
272    @Override
273    public int hashCode() {
274        return Objects.hash(pathCounters);
275    }
276
277    @Override
278    public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) throws IOException {
279        updateDirCounter(directoryPostTransformer.apply(dir), exc);
280        return FileVisitResult.CONTINUE;
281    }
282
283    @Override
284    public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attributes) throws IOException {
285        final FileVisitResult accept = directoryFilter.accept(dir, attributes);
286        return accept != FileVisitResult.CONTINUE ? FileVisitResult.SKIP_SUBTREE : FileVisitResult.CONTINUE;
287    }
288
289    @Override
290    public String toString() {
291        return pathCounters.toString();
292    }
293
294    /**
295     * Updates the counter for visiting the given directory.
296     *
297     * @param dir the visited directory.
298     * @param exc Encountered exception.
299     * @since 2.9.0
300     */
301    protected void updateDirCounter(final Path dir, final IOException exc) {
302        pathCounters.getDirectoryCounter().increment();
303    }
304
305    /**
306     * Updates the counters for visiting the given file.
307     *
308     * @param file       the visited file.
309     * @param attributes the visited file attributes.
310     */
311    protected void updateFileCounters(final Path file, final BasicFileAttributes attributes) {
312        pathCounters.getFileCounter().increment();
313        pathCounters.getByteCounter().add(attributes.size());
314    }
315
316    @Override
317    public FileVisitResult visitFile(final Path file, final BasicFileAttributes attributes) throws IOException {
318        if (accept(file, attributes)) {
319            updateFileCounters(file, attributes);
320        }
321        return FileVisitResult.CONTINUE;
322    }
323
324}