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