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