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}