load.go 7.1 KB

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