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 */
017package org.apache.juneau.commons.io;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022import java.nio.charset.*;
023import java.nio.file.*;
024
025/**
026 * A fluent builder for creating {@link Reader} instances from {@link Path} objects with configurable character encoding.
027 *
028 * <p>
029 * This builder provides a convenient way to create readers from NIO {@link Path} objects with custom
030 * character encodings and optional handling for missing files. It's similar to {@link FileReaderBuilder}
031 * but works with the modern NIO Path API instead of the legacy File API.
032 *
033 * <h5 class='section'>Features:</h5>
034 * <ul class='spaced-list'>
035 *    <li>Fluent API - all methods return <c>this</c> for method chaining
036 *    <li>NIO Path support - works with modern {@link Path} API
037 *    <li>Character encoding support - specify custom charset for file reading
038 *    <li>Missing file handling - optional support for returning empty reader when file doesn't exist
039 *    <li>Multiple path specification methods - accept Path or String path
040 * </ul>
041 *
042 * <h5 class='section'>Use Cases:</h5>
043 * <ul class='spaced-list'>
044 *    <li>Reading files with specific character encodings using NIO Path API
045 *    <li>Handling optional configuration files that may not exist
046 *    <li>Creating readers with consistent encoding across an application
047 *    <li>Working with NIO-based file operations
048 * </ul>
049 *
050 * <h5 class='section'>Usage:</h5>
051 * <p class='bjava'>
052 *    <jc>// Basic usage</jc>
053 *    Reader <jv>reader</jv> = PathReaderBuilder.<jsm>create</jsm>()
054 *       .path(Paths.get(<js>"/path/to/file.txt"</js>))
055 *       .charset(<js>"UTF-8"</js>)
056 *       .build();
057 *
058 *    <jc>// With missing file handling</jc>
059 *    Reader <jv>reader2</jv> = PathReaderBuilder.<jsm>create</jsm>()
060 *       .path(<js>"optional-config.properties"</js>)
061 *       .allowNoFile()
062 *       .build();  <jc>// Returns empty StringReader if file doesn't exist</jc>
063 *
064 *    <jc>// Using Path object</jc>
065 *    Path <jv>path</jv> = Paths.get(<js>"data.txt"</js>);
066 *    Reader <jv>reader3</jv> = PathReaderBuilder.<jsm>create</jsm>(<jv>path</jv>)
067 *       .charset(StandardCharsets.UTF_8)
068 *       .build();
069 * </p>
070 *
071 * <h5 class='section'>Character Encoding:</h5>
072 * <p>
073 * By default, the builder uses the system's default charset ({@link Charset#defaultCharset()}).
074 * You can specify a custom charset using {@link #charset(Charset)} or {@link #charset(String)}.
075 * This is important when reading files that were written with a specific encoding.
076 *
077 * <h5 class='section'>Comparison with FileReaderBuilder:</h5>
078 * <ul class='spaced-list'>
079 *    <li><b>FileReaderBuilder:</b> Works with legacy {@link File} API
080 *    <li><b>PathReaderBuilder:</b> Works with modern NIO {@link Path} API
081 *    <li><b>FileReaderBuilder:</b> Uses {@link FileInputStream}
082 *    <li><b>PathReaderBuilder:</b> Uses {@link Files#newBufferedReader(Path, Charset)}
083 * </ul>
084 *
085 * <h5 class='section'>See Also:</h5><ul>
086 *    <li class='jc'>{@link FileReaderBuilder} - Builder for File-based readers
087 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauCommonsIO">I/O Package</a>
088 * </ul>
089 *
090 * @since 9.1.0
091 */
092@SuppressWarnings({
093   "java:S1192" // Duplicated literals (argument/property names) read more clearly inline than as constants
094})
095public class PathReaderBuilder {
096
097   /**
098    * Creates a new builder.
099    *
100    * <h5 class='section'>Example:</h5>
101    * <p class='bjava'>
102    *    Reader <jv>reader</jv> = PathReaderBuilder.<jsm>create</jsm>()
103    *       .path(Paths.get(<js>"data.txt"</js>))
104    *       .build();
105    * </p>
106    *
107    * @return A new builder instance.
108    */
109   public static PathReaderBuilder create() {
110      return new PathReaderBuilder();
111   }
112
113   /**
114    * Creates a new builder initialized with the specified path.
115    *
116    * <h5 class='section'>Example:</h5>
117    * <p class='bjava'>
118    *    Path <jv>path</jv> = Paths.get(<js>"config.properties"</js>);
119    *    Reader <jv>reader</jv> = PathReaderBuilder.<jsm>create</jsm>(<jv>path</jv>)
120    *       .charset(<js>"UTF-8"</js>)
121    *       .build();
122    * </p>
123    *
124    * @param path The path to read from.  Can be <jk>null</jk> ({@link #build()} then throws an {@link IllegalStateException} unless {@link #allowNoFile()} is set).
125    * @return A new builder instance initialized with the specified path.
126    */
127   public static PathReaderBuilder create(Path path) {
128      return new PathReaderBuilder().path(path);
129   }
130
131   private Path path;
132
133   private Charset charset = Charset.defaultCharset();
134
135   private boolean allowNoFile;
136
137   /**
138    * Enables handling of missing files by returning an empty reader instead of throwing an exception.
139    *
140    * <p>
141    * When this option is enabled, if the path is <jk>null</jk> or does not exist, the {@link #build()}
142    * method will return a {@link StringReader} with empty content instead of throwing an
143    * {@link IOException}. This is useful for optional configuration files.
144    *
145    * <h5 class='section'>Example:</h5>
146    * <p class='bjava'>
147    *    <jc>// Without allowNoFile - throws exception if file doesn't exist</jc>
148    *    Reader <jv>reader1</jv> = PathReaderBuilder.<jsm>create</jsm>()
149    *       .path(Paths.get(<js>"required.txt"</js>))
150    *       .build();  <jc>// Throws NoSuchFileException if file missing</jc>
151    *
152    *    <jc>// With allowNoFile - returns empty reader if file doesn't exist</jc>
153    *    Reader <jv>reader2</jv> = PathReaderBuilder.<jsm>create</jsm>()
154    *       .path(Paths.get(<js>"optional.txt"</js>))
155    *       .allowNoFile()
156    *       .build();  <jc>// Returns empty StringReader if file missing</jc>
157    * </p>
158    *
159    * @return This object for method chaining.
160    */
161   public PathReaderBuilder allowNoFile() {
162      return allowNoFile(true);
163   }
164
165   /**
166    * Enables or disables handling of missing files by returning an empty reader instead of throwing an exception.
167    *
168    * <p>
169    * Boolean-arg form of {@link #allowNoFile()}, useful when the setting is conditional.
170    *
171    * <h5 class='section'>Example:</h5>
172    * <p class='bjava'>
173    *    Reader <jv>reader</jv> = PathReaderBuilder.<jsm>create</jsm>()
174    *       .path(Paths.get(<js>"optional.txt"</js>))
175    *       .allowNoFile(<jv>isOptional</jv>)
176    *       .build();
177    * </p>
178    *
179    * @param value <jk>true</jk> to return an empty reader when the file is missing.
180    * @return This object for method chaining.
181    */
182   public PathReaderBuilder allowNoFile(boolean value) {
183      this.allowNoFile = value;
184      return this;
185   }
186
187   /**
188    * Creates a new {@link Reader} for reading from the configured path.
189    *
190    * <p>
191    * If {@link #allowNoFile()} was called and the path is <jk>null</jk> or does not exist,
192    * this method returns an empty {@link StringReader}. Otherwise, it creates a buffered reader
193    * using {@link Files#newBufferedReader(Path, Charset)} with the specified character encoding.
194    *
195    * <h5 class='section'>Example:</h5>
196    * <p class='bjava'>
197    *    <jk>try</jk> (Reader <jv>reader</jv> = PathReaderBuilder.<jsm>create</jsm>()
198    *       .path(Paths.get(<js>"data.txt"</js>))
199    *       .charset(<js>"UTF-8"</js>)
200    *       .build()) {
201    *       <jc>// Read from file</jc>
202    *    }
203    * </p>
204    *
205    * @return A new {@link Reader} for reading from the path.
206    * @throws IllegalStateException If no path is configured and {@link #allowNoFile()} was not called.
207    * @throws NoSuchFileException If the path does not exist and {@link #allowNoFile()} was not called.
208    * @throws IOException If an I/O error occurs opening the path.
209    */
210   @SuppressWarnings({
211      "resource" // Caller takes ownership of the returned Reader
212   })
213   public Reader build() throws IOException {
214      if (! allowNoFile && path == null) {
215         throw new IllegalStateException("No path");
216      }
217      if (! allowNoFile && ! Files.exists(path)) {
218         throw new NoSuchFileException(path.toString());
219      }
220      return allowNoFile ? new StringReader("") : Files.newBufferedReader(path, o(charset).orElse(Charset.defaultCharset()));
221   }
222
223   /**
224    * Sets the character encoding for reading the path.
225    *
226    * <p>
227    * If not specified, the system's default charset ({@link Charset#defaultCharset()}) is used.
228    * Specifying the encoding is important when reading files that were written with a specific
229    * character encoding. Passing <jk>null</jk> resets to the default charset.
230    *
231    * <h5 class='section'>Example:</h5>
232    * <p class='bjava'>
233    *    Reader <jv>reader</jv> = PathReaderBuilder.<jsm>create</jsm>()
234    *       .path(Paths.get(<js>"data.txt"</js>))
235    *       .charset(StandardCharsets.UTF_8)
236    *       .build();
237    * </p>
238    *
239    * @param charset The character encoding to use. The default is {@link Charset#defaultCharset()}.
240    *                <jk>null</jk> resets to the default.
241    * @return This object for method chaining.
242    */
243   public PathReaderBuilder charset(Charset charset) {
244      this.charset = charset;
245      return this;
246   }
247
248   /**
249    * Sets the character encoding for reading the path by charset name.
250    *
251    * <p>
252    * This is a convenience method that accepts a charset name string and converts it to a
253    * {@link Charset} using {@link Charset#forName(String)}. Passing <jk>null</jk> resets to
254    * the default charset.
255    *
256    * <h5 class='section'>Example:</h5>
257    * <p class='bjava'>
258    *    Reader <jv>reader</jv> = PathReaderBuilder.<jsm>create</jsm>()
259    *       .path(Paths.get(<js>"data.txt"</js>))
260    *       .charset(<js>"UTF-8"</js>)
261    *       .build();
262    * </p>
263    *
264    * @param charset The character encoding name (e.g., <js>"UTF-8"</js>, <js>"ISO-8859-1"</js>).
265    *                The default is {@link Charset#defaultCharset()}.
266    *                <jk>null</jk> resets to the default.
267    * @return This object for method chaining.
268    */
269   public PathReaderBuilder charset(String charset) {
270      this.charset = nn(charset) ? Charset.forName(charset) : null;
271      return this;
272   }
273
274   /**
275    * Sets the path to read from.
276    *
277    * <h5 class='section'>Example:</h5>
278    * <p class='bjava'>
279    *    Path <jv>p</jv> = Paths.get(<js>"config.properties"</js>);
280    *    Reader <jv>reader</jv> = PathReaderBuilder.<jsm>create</jsm>()
281    *       .path(<jv>p</jv>)
282    *       .build();
283    * </p>
284    *
285    * @param path The path to read from.  Can be <jk>null</jk> ({@link #build()} then throws an {@link IllegalStateException} unless {@link #allowNoFile()} is set).
286    * @return This object for method chaining.
287    */
288   public PathReaderBuilder path(Path path) {
289      this.path = path;
290      return this;
291   }
292
293   /**
294    * Sets the path to read from by string path.
295    *
296    * <p>
297    * This is a convenience method that converts a string path to a {@link Path} using
298    * {@link Paths#get(String, String...)}.
299    *
300    * <h5 class='section'>Example:</h5>
301    * <p class='bjava'>
302    *    Reader <jv>reader</jv> = PathReaderBuilder.<jsm>create</jsm>()
303    *       .path(<js>"/path/to/file.txt"</js>)
304    *       .build();
305    * </p>
306    *
307    * @param path The file path to read from. Must not be <jk>null</jk>.
308    * @return This object for method chaining.
309    */
310   public PathReaderBuilder path(String path) {
311      this.path = Paths.get(reqnn("path", path));
312      return this;
313   }
314}