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 }
294