load.go (9465B)
1 // Copyright 2013-2022 Frank Schroeder. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package properties 6 7 import ( 8 "fmt" 9 "io/ioutil" 10 "net/http" 11 "os" 12 "strings" 13 ) 14 15 // Encoding specifies encoding of the input data. 16 type Encoding uint 17 18 const ( 19 // utf8Default is a private placeholder for the zero value of Encoding to 20 // ensure that it has the correct meaning. UTF8 is the default encoding but 21 // was assigned a non-zero value which cannot be changed without breaking 22 // existing code. Clients should continue to use the public constants. 23 utf8Default Encoding = iota 24 25 // UTF8 interprets the input data as UTF-8. 26 UTF8 27 28 // ISO_8859_1 interprets the input data as ISO-8859-1. 29 ISO_8859_1 30 ) 31 32 type Loader struct { 33 // Encoding determines how the data from files and byte buffers 34 // is interpreted. For URLs the Content-Type header is used 35 // to determine the encoding of the data. 36 Encoding Encoding 37 38 // DisableExpansion configures the property expansion of the 39 // returned property object. When set to true, the property values 40 // will not be expanded and the Property object will not be checked 41 // for invalid expansion expressions. 42 DisableExpansion bool 43 44 // IgnoreMissing configures whether missing files or URLs which return 45 // 404 are reported as errors. When set to true, missing files and 404 46 // status codes are not reported as errors. 47 IgnoreMissing bool 48 } 49 50 // Load reads a buffer into a Properties struct. 51 func (l *Loader) LoadBytes(buf []byte) (*Properties, error) { 52 return l.loadBytes(buf, l.Encoding) 53 } 54 55 // LoadAll reads the content of multiple URLs or files in the given order into 56 // a Properties struct. If IgnoreMissing is true then a 404 status code or 57 // missing file will not be reported as error. Encoding sets the encoding for 58 // files. For the URLs see LoadURL for the Content-Type header and the 59 // encoding. 60 func (l *Loader) LoadAll(names []string) (*Properties, error) { 61 all := NewProperties() 62 for _, name := range names { 63 n, err := expandName(name) 64 if err != nil { 65 return nil, err 66 } 67 68 var p *Properties 69 switch { 70 case strings.HasPrefix(n, "http://"): 71 p, err = l.LoadURL(n) 72 case strings.HasPrefix(n, "https://"): 73 p, err = l.LoadURL(n) 74 default: 75 p, err = l.LoadFile(n) 76 } 77 if err != nil { 78 return nil, err 79 } 80 all.Merge(p) 81 } 82 83 all.DisableExpansion = l.DisableExpansion 84 if all.DisableExpansion { 85 return all, nil 86 } 87 return all, all.check() 88 } 89 90 // LoadFile reads a file into a Properties struct. 91 // If IgnoreMissing is true then a missing file will not be 92 // reported as error. 93 func (l *Loader) LoadFile(filename string) (*Properties, error) { 94 data, err := ioutil.ReadFile(filename) 95 if err != nil { 96 if l.IgnoreMissing && os.IsNotExist(err) { 97 LogPrintf("properties: %s not found. skipping", filename) 98 return NewProperties(), nil 99 } 100 return nil, err 101 } 102 return l.loadBytes(data, l.Encoding) 103 } 104 105 // LoadURL reads the content of the URL into a Properties struct. 106 // 107 // The encoding is determined via the Content-Type header which 108 // should be set to 'text/plain'. If the 'charset' parameter is 109 // missing, 'iso-8859-1' or 'latin1' the encoding is set to 110 // ISO-8859-1. If the 'charset' parameter is set to 'utf-8' the 111 // encoding is set to UTF-8. A missing content type header is 112 // interpreted as 'text/plain; charset=utf-8'. 113 func (l *Loader) LoadURL(url string) (*Properties, error) { 114 resp, err := http.Get(url) 115 if err != nil { 116 return nil, fmt.Errorf("properties: error fetching %q. %s", url, err) 117 } 118 defer resp.Body.Close() 119 120 if resp.StatusCode == 404 && l.IgnoreMissing { 121 LogPrintf("properties: %s returned %d. skipping", url, resp.StatusCode) 122 return NewProperties(), nil 123 } 124 125 if resp.StatusCode != 200 { 126 return nil, fmt.Errorf("properties: %s returned %d", url, resp.StatusCode) 127 } 128 129 body, err := ioutil.ReadAll(resp.Body) 130 if err != nil { 131 return nil, fmt.Errorf("properties: %s error reading response. %s", url, err) 132 } 133 134 ct := resp.Header.Get("Content-Type") 135 ct = strings.Join(strings.Fields(ct), "") 136 var enc Encoding 137 switch strings.ToLower(ct) { 138 case "text/plain", "text/plain;charset=iso-8859-1", "text/plain;charset=latin1": 139 enc = ISO_8859_1 140 case "", "text/plain;charset=utf-8": 141 enc = UTF8 142 default: 143 return nil, fmt.Errorf("properties: invalid content type %s", ct) 144 } 145 146 return l.loadBytes(body, enc) 147 } 148 149 func (l *Loader) loadBytes(buf []byte, enc Encoding) (*Properties, error) { 150 p, err := parse(convert(buf, enc)) 151 if err != nil { 152 return nil, err 153 } 154 p.DisableExpansion = l.DisableExpansion 155 if p.DisableExpansion { 156 return p, nil 157 } 158 return p, p.check() 159 } 160 161 // Load reads a buffer into a Properties struct. 162 func Load(buf []byte, enc Encoding) (*Properties, error) { 163 l := &Loader{Encoding: enc} 164 return l.LoadBytes(buf) 165 } 166 167 // LoadString reads an UTF8 string into a properties struct. 168 func LoadString(s string) (*Properties, error) { 169 l := &Loader{Encoding: UTF8} 170 return l.LoadBytes([]byte(s)) 171 } 172 173 // LoadMap creates a new Properties struct from a string map. 174 func LoadMap(m map[string]string) *Properties { 175 p := NewProperties() 176 for k, v := range m { 177 p.Set(k, v) 178 } 179 return p 180 } 181 182 // LoadFile reads a file into a Properties struct. 183 func LoadFile(filename string, enc Encoding) (*Properties, error) { 184 l := &Loader{Encoding: enc} 185 return l.LoadAll([]string{filename}) 186 } 187 188 // LoadFiles reads multiple files in the given order into 189 // a Properties struct. If 'ignoreMissing' is true then 190 // non-existent files will not be reported as error. 191 func LoadFiles(filenames []string, enc Encoding, ignoreMissing bool) (*Properties, error) { 192 l := &Loader{Encoding: enc, IgnoreMissing: ignoreMissing} 193 return l.LoadAll(filenames) 194 } 195 196 // LoadURL reads the content of the URL into a Properties struct. 197 // See Loader#LoadURL for details. 198 func LoadURL(url string) (*Properties, error) { 199 l := &Loader{Encoding: UTF8} 200 return l.LoadAll([]string{url}) 201 } 202 203 // LoadURLs reads the content of multiple URLs in the given order into a 204 // Properties struct. If IgnoreMissing is true then a 404 status code will 205 // not be reported as error. See Loader#LoadURL for the Content-Type header 206 // and the encoding. 207 func LoadURLs(urls []string, ignoreMissing bool) (*Properties, error) { 208 l := &Loader{Encoding: UTF8, IgnoreMissing: ignoreMissing} 209 return l.LoadAll(urls) 210 } 211 212 // LoadAll reads the content of multiple URLs or files in the given order into a 213 // Properties struct. If 'ignoreMissing' is true then a 404 status code or missing file will 214 // not be reported as error. Encoding sets the encoding for files. For the URLs please see 215 // LoadURL for the Content-Type header and the encoding. 216 func LoadAll(names []string, enc Encoding, ignoreMissing bool) (*Properties, error) { 217 l := &Loader{Encoding: enc, IgnoreMissing: ignoreMissing} 218 return l.LoadAll(names) 219 } 220 221 // MustLoadString reads an UTF8 string into a Properties struct and 222 // panics on error. 223 func MustLoadString(s string) *Properties { 224 return must(LoadString(s)) 225 } 226 227 // MustLoadFile reads a file into a Properties struct and 228 // panics on error. 229 func MustLoadFile(filename string, enc Encoding) *Properties { 230 return must(LoadFile(filename, enc)) 231 } 232 233 // MustLoadFiles reads multiple files in the given order into 234 // a Properties struct and panics on error. If 'ignoreMissing' 235 // is true then non-existent files will not be reported as error. 236 func MustLoadFiles(filenames []string, enc Encoding, ignoreMissing bool) *Properties { 237 return must(LoadFiles(filenames, enc, ignoreMissing)) 238 } 239 240 // MustLoadURL reads the content of a URL into a Properties struct and 241 // panics on error. 242 func MustLoadURL(url string) *Properties { 243 return must(LoadURL(url)) 244 } 245 246 // MustLoadURLs reads the content of multiple URLs in the given order into a 247 // Properties struct and panics on error. If 'ignoreMissing' is true then a 404 248 // status code will not be reported as error. 249 func MustLoadURLs(urls []string, ignoreMissing bool) *Properties { 250 return must(LoadURLs(urls, ignoreMissing)) 251 } 252 253 // MustLoadAll reads the content of multiple URLs or files in the given order into a 254 // Properties struct. If 'ignoreMissing' is true then a 404 status code or missing file will 255 // not be reported as error. Encoding sets the encoding for files. For the URLs please see 256 // LoadURL for the Content-Type header and the encoding. It panics on error. 257 func MustLoadAll(names []string, enc Encoding, ignoreMissing bool) *Properties { 258 return must(LoadAll(names, enc, ignoreMissing)) 259 } 260 261 func must(p *Properties, err error) *Properties { 262 if err != nil { 263 ErrorHandler(err) 264 } 265 return p 266 } 267 268 // expandName expands ${ENV_VAR} expressions in a name. 269 // If the environment variable does not exist then it will be replaced 270 // with an empty string. Malformed expressions like "${ENV_VAR" will 271 // be reported as error. 272 func expandName(name string) (string, error) { 273 return expand(name, []string{}, "${", "}", make(map[string]string)) 274 } 275 276 // Interprets a byte buffer either as an ISO-8859-1 or UTF-8 encoded string. 277 // For ISO-8859-1 we can convert each byte straight into a rune since the 278 // first 256 unicode code points cover ISO-8859-1. 279 func convert(buf []byte, enc Encoding) string { 280 switch enc { 281 case utf8Default, UTF8: 282 return string(buf) 283 case ISO_8859_1: 284 runes := make([]rune, len(buf)) 285 for i, b := range buf { 286 runes[i] = rune(b) 287 } 288 return string(runes) 289 default: 290 ErrorHandler(fmt.Errorf("unsupported encoding %v", enc)) 291 } 292 panic("ErrorHandler should exit") 293 }