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}