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