load.go raw

   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