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