1// Copyright 2017 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 5package properties 6 7import ( 8 "fmt" 9 "io/ioutil" 10 "net/http" 11 "os" 12 "strings" 13) 14 15// Encoding specifies encoding of the input data. 16type Encoding uint 17 18const ( 19 // UTF8 interprets the input data as UTF-8. 20 UTF8 Encoding = 1 << iota 21 22 // ISO_8859_1 interprets the input data as ISO-8859-1. 23 ISO_8859_1 24) 25 26// Load reads a buffer into a Properties struct. 27func Load(buf []byte, enc Encoding) (*Properties, error) { 28 return loadBuf(buf, enc) 29} 30 31// LoadString reads an UTF8 string into a properties struct. 32func LoadString(s string) (*Properties, error) { 33 return loadBuf([]byte(s), UTF8) 34} 35 36// LoadMap creates a new Properties struct from a string map. 37func LoadMap(m map[string]string) *Properties { 38 p := NewProperties() 39 for k, v := range m { 40 p.Set(k, v) 41 } 42 return p 43} 44 45// LoadFile reads a file into a Properties struct. 46func LoadFile(filename string, enc Encoding) (*Properties, error) { 47 return loadAll([]string{filename}, enc, false) 48} 49 50// LoadFiles reads multiple files in the given order into 51// a Properties struct. If 'ignoreMissing' is true then 52// non-existent files will not be reported as error. 53func LoadFiles(filenames []string, enc Encoding, ignoreMissing bool) (*Properties, error) { 54 return loadAll(filenames, enc, ignoreMissing) 55} 56 57// LoadURL reads the content of the URL into a Properties struct. 58// 59// The encoding is determined via the Content-Type header which 60// should be set to 'text/plain'. If the 'charset' parameter is 61// missing, 'iso-8859-1' or 'latin1' the encoding is set to 62// ISO-8859-1. If the 'charset' parameter is set to 'utf-8' the 63// encoding is set to UTF-8. A missing content type header is 64// interpreted as 'text/plain; charset=utf-8'. 65func LoadURL(url string) (*Properties, error) { 66 return loadAll([]string{url}, UTF8, false) 67} 68 69// LoadURLs reads the content of multiple URLs in the given order into a 70// Properties struct. If 'ignoreMissing' is true then a 404 status code will 71// not be reported as error. See LoadURL for the Content-Type header 72// and the encoding. 73func LoadURLs(urls []string, ignoreMissing bool) (*Properties, error) { 74 return loadAll(urls, UTF8, ignoreMissing) 75} 76 77// LoadAll reads the content of multiple URLs or files in the given order into a 78// Properties struct. If 'ignoreMissing' is true then a 404 status code or missing file will 79// not be reported as error. Encoding sets the encoding for files. For the URLs please see 80// LoadURL for the Content-Type header and the encoding. 81func LoadAll(names []string, enc Encoding, ignoreMissing bool) (*Properties, error) { 82 return loadAll(names, enc, ignoreMissing) 83} 84 85// MustLoadString reads an UTF8 string into a Properties struct and 86// panics on error. 87func MustLoadString(s string) *Properties { 88 return must(LoadString(s)) 89} 90 91// MustLoadFile reads a file into a Properties struct and 92// panics on error. 93func MustLoadFile(filename string, enc Encoding) *Properties { 94 return must(LoadFile(filename, enc)) 95} 96 97// MustLoadFiles reads multiple files in the given order into 98// a Properties struct and panics on error. If 'ignoreMissing' 99// is true then non-existent files will not be reported as error. 100func MustLoadFiles(filenames []string, enc Encoding, ignoreMissing bool) *Properties { 101 return must(LoadFiles(filenames, enc, ignoreMissing)) 102} 103 104// MustLoadURL reads the content of a URL into a Properties struct and 105// panics on error. 106func MustLoadURL(url string) *Properties { 107 return must(LoadURL(url)) 108} 109 110// MustLoadURLs reads the content of multiple URLs in the given order into a 111// Properties struct and panics on error. If 'ignoreMissing' is true then a 404 112// status code will not be reported as error. 113func MustLoadURLs(urls []string, ignoreMissing bool) *Properties { 114 return must(LoadURLs(urls, ignoreMissing)) 115} 116 117// MustLoadAll reads the content of multiple URLs or files in the given order into a 118// Properties struct. If 'ignoreMissing' is true then a 404 status code or missing file will 119// not be reported as error. Encoding sets the encoding for files. For the URLs please see 120// LoadURL for the Content-Type header and the encoding. It panics on error. 121func MustLoadAll(names []string, enc Encoding, ignoreMissing bool) *Properties { 122 return must(LoadAll(names, enc, ignoreMissing)) 123} 124 125func loadBuf(buf []byte, enc Encoding) (*Properties, error) { 126 p, err := parse(convert(buf, enc)) 127 if err != nil { 128 return nil, err 129 } 130 return p, p.check() 131} 132 133func loadAll(names []string, enc Encoding, ignoreMissing bool) (*Properties, error) { 134 result := NewProperties() 135 for _, name := range names { 136 n, err := expandName(name) 137 if err != nil { 138 return nil, err 139 } 140 var p *Properties 141 if strings.HasPrefix(n, "http://") || strings.HasPrefix(n, "https://") { 142 p, err = loadURL(n, ignoreMissing) 143 } else { 144 p, err = loadFile(n, enc, ignoreMissing) 145 } 146 if err != nil { 147 return nil, err 148 } 149 result.Merge(p) 150 151 } 152 return result, result.check() 153} 154 155func loadFile(filename string, enc Encoding, ignoreMissing bool) (*Properties, error) { 156 data, err := ioutil.ReadFile(filename) 157 if err != nil { 158 if ignoreMissing && os.IsNotExist(err) { 159 LogPrintf("properties: %s not found. skipping", filename) 160 return NewProperties(), nil 161 } 162 return nil, err 163 } 164 p, err := parse(convert(data, enc)) 165 if err != nil { 166 return nil, err 167 } 168 return p, nil 169} 170 171func loadURL(url string, ignoreMissing bool) (*Properties, error) { 172 resp, err := http.Get(url) 173 if err != nil { 174 return nil, fmt.Errorf("properties: error fetching %q. %s", url, err) 175 } 176 if resp.StatusCode == 404 && ignoreMissing { 177 LogPrintf("properties: %s returned %d. skipping", url, resp.StatusCode) 178 return NewProperties(), nil 179 } 180 if resp.StatusCode != 200 { 181 return nil, fmt.Errorf("properties: %s returned %d", url, resp.StatusCode) 182 } 183 body, err := ioutil.ReadAll(resp.Body) 184 if err != nil { 185 return nil, fmt.Errorf("properties: %s error reading response. %s", url, err) 186 } 187 if err = resp.Body.Close(); err != nil { 188 return nil, fmt.Errorf("properties: %s error reading response. %s", url, err) 189 } 190 191 ct := resp.Header.Get("Content-Type") 192 var enc Encoding 193 switch strings.ToLower(ct) { 194 case "text/plain", "text/plain; charset=iso-8859-1", "text/plain; charset=latin1": 195 enc = ISO_8859_1 196 case "", "text/plain; charset=utf-8": 197 enc = UTF8 198 default: 199 return nil, fmt.Errorf("properties: invalid content type %s", ct) 200 } 201 202 p, err := parse(convert(body, enc)) 203 if err != nil { 204 return nil, err 205 } 206 return p, nil 207} 208 209func must(p *Properties, err error) *Properties { 210 if err != nil { 211 ErrorHandler(err) 212 } 213 return p 214} 215 216// expandName expands ${ENV_VAR} expressions in a name. 217// If the environment variable does not exist then it will be replaced 218// with an empty string. Malformed expressions like "${ENV_VAR" will 219// be reported as error. 220func expandName(name string) (string, error) { 221 return expand(name, make(map[string]bool), "${", "}", make(map[string]string)) 222} 223 224// Interprets a byte buffer either as an ISO-8859-1 or UTF-8 encoded string. 225// For ISO-8859-1 we can convert each byte straight into a rune since the 226// first 256 unicode code points cover ISO-8859-1. 227func convert(buf []byte, enc Encoding) string { 228 switch enc { 229 case UTF8: 230 return string(buf) 231 case ISO_8859_1: 232 runes := make([]rune, len(buf)) 233 for i, b := range buf { 234 runes[i] = rune(b) 235 } 236 return string(runes) 237 default: 238 ErrorHandler(fmt.Errorf("unsupported encoding %v", enc)) 239 } 240 panic("ErrorHandler should exit") 241} 242