Issue #10: Support loading from urls
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b82f095..a34b828 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,9 @@
### [1.6.1](https://github.com/magiconair/properties/tags/v1.6.1) - unreleased
- * [Issue #11](https://github.com/magiconair/properties/issues/11): Add [LoadString](http://godoc.org/github.com/magiconair/properties#Properties.LoadString) method to load properties from an UTF8 string.
+ * [Issue #10](https://github.com/magiconair/properties/issues/10): Add [LoadURL,LoadURLs,MustLoadURL,MustLoadURLs](http://godoc.org/github.com/magiconair/properties#Properties.LoadURL) method to load properties from a URL.
+ * [Issue #11](https://github.com/magiconair/properties/issues/11): Add [LoadString,MustLoadString](http://godoc.org/github.com/magiconair/properties#Properties.LoadString) method to load properties from an UTF8 string.
+ * [PR #8](https://github.com/magiconair/properties/pull/8): Add [MustFlag](http://godoc.org/github.com/magiconair/properties#Properties.MustFlag) method to provide overrides via command line flags. (@pascaldekloe)
### [1.6.0](https://github.com/magiconair/properties/tags/v1.6.0) - 11 Dec 2015
@@ -14,7 +16,7 @@
### [1.5.5](https://github.com/magiconair/properties/tags/v1.5.5) - 31 Jul 2015
- * [Pull Request #6](https://github.com/magiconair/properties/pull/6): Add [Delete](http://godoc.org/github.com/magiconair/properties#Properties.Delete) method to remove keys including comments. (@gerbenjacobs)
+ * [PR #6](https://github.com/magiconair/properties/pull/6): Add [Delete](http://godoc.org/github.com/magiconair/properties#Properties.Delete) method to remove keys including comments. (@gerbenjacobs)
### [1.5.4](https://github.com/magiconair/properties/tags/v1.5.4) - 23 Jun 2015
diff --git a/README.md b/README.md
index b3b2e5c..1dd13d7 100644
--- a/README.md
+++ b/README.md
@@ -5,12 +5,11 @@
properties is a Go library for reading and writing properties files.
-It supports reading from multiple files and Spring style recursive property
-expansion of expressions like `${key}` to their corresponding value.
-Value expressions can refer to other keys like in `${key}` or to
-environment variables like in `${USER}`.
-Filenames can also contain environment variables like in
-`/home/${USER}/myapp.properties`.
+It supports reading from multiple files or URLs and Spring style recursive
+property expansion of expressions like `${key}` to their corresponding value.
+Value expressions can refer to other keys like in `${key}` or to environment
+variables like in `${USER}`. Filenames can also contain environment variables
+like in `/home/${USER}/myapp.properties`.
Properties can be decoded into structs, maps, arrays and values through
struct tags.
@@ -56,6 +55,9 @@
// or via flags
p.MustFlag(flag.CommandLine)
+
+ // or via url
+ p = properties.MustLoadURL("http://host/path")
}
```
diff --git a/load.go b/load.go
index e74d77b..3915c73 100644
--- a/load.go
+++ b/load.go
@@ -5,9 +5,12 @@
package properties
import (
+ "bytes"
"fmt"
"io/ioutil"
+ "net/http"
"os"
+ "strings"
)
// Encoding specifies encoding of the input data.
@@ -43,6 +46,26 @@
return loadFiles(filenames, enc, ignoreMissing)
}
+// LoadURL reads the content of the URL into a Properties struct.
+//
+// The encoding is determined via the Content-Type header which
+// should be set to 'text/plain'. If the 'charset' parameter is
+// missing, 'iso-8859-1' or 'latin1' the encoding is set to
+// ISO-8859-1. If the 'charset' parameter is set to 'utf-8' the
+// encoding is set to UTF-8. A missing content type header is
+// interpreted as 'text/plain; charset=utf-8'.
+func LoadURL(url string) (*Properties, error) {
+ return loadURLs([]string{url}, false)
+}
+
+// LoadURLs reads the content of multiple URLs in the given order into a
+// Properties struct. If 'ignoreMissing' is true then a 404 status code will
+// not be reported as error. See LoadURL for the Content-Type header
+// and the encoding.
+func LoadURLs(urls []string, ignoreMissing bool) (*Properties, error) {
+ return loadURLs(urls, ignoreMissing)
+}
+
// MustLoadString reads an UTF8 string into a Properties struct and
// panics on error.
func MustLoadString(s string) *Properties {
@@ -62,6 +85,19 @@
return must(LoadFiles(filenames, enc, ignoreMissing))
}
+// MustLoadURL reads the content of a URL into a Properties struct and
+// panics on error.
+func MustLoadURL(url string) *Properties {
+ return must(LoadURL(url))
+}
+
+// MustLoadFiles reads the content of multiple URLs in the given order into a
+// Properties struct and panics on error. If 'ignoreMissing' is true then a 404
+// status code will not be reported as error.
+func MustLoadURLs(urls []string, ignoreMissing bool) *Properties {
+ return must(LoadURLs(urls, ignoreMissing))
+}
+
func loadBuf(buf []byte, enc Encoding) (*Properties, error) {
p, err := parse(convert(buf, enc))
if err != nil {
@@ -71,18 +107,17 @@
}
func loadFiles(filenames []string, enc Encoding, ignoreMissing bool) (*Properties, error) {
- buff := make([]byte, 0, 4096)
-
+ var buf bytes.Buffer
for _, filename := range filenames {
f, err := expandFilename(filename)
if err != nil {
return nil, err
}
- buf, err := ioutil.ReadFile(f)
+ data, err := ioutil.ReadFile(f)
if err != nil {
if ignoreMissing && os.IsNotExist(err) {
- // TODO(frank): should we log that we are skipping the file?
+ LogPrintf("properties: %s not found. skipping", filename)
continue
}
return nil, err
@@ -90,10 +125,47 @@
// concatenate the buffers and add a new line in case
// the previous file didn't end with a new line
- buff = append(append(buff, buf...), '\n')
+ buf.Write(data)
+ buf.WriteRune('\n')
}
+ return loadBuf(buf.Bytes(), enc)
+}
- return loadBuf(buff, enc)
+func loadURLs(urls []string, ignoreMissing bool) (*Properties, error) {
+ var buf bytes.Buffer
+ for _, u := range urls {
+ resp, err := http.Get(u)
+ if err != nil {
+ return nil, fmt.Errorf("properties: error fetching %q. %s", u, err)
+ }
+ if resp.StatusCode == 404 && ignoreMissing {
+ LogPrintf("properties: %s returned %d. skipping", u, resp.StatusCode)
+ continue
+ }
+ if resp.StatusCode != 200 {
+ return nil, fmt.Errorf("properties: %s returned %d", u, resp.StatusCode)
+ }
+ body, err := ioutil.ReadAll(resp.Body)
+ resp.Body.Close()
+ if err != nil {
+ return nil, fmt.Errorf("properties: %s error reading response. %s", u, err)
+ }
+
+ ct := resp.Header.Get("Content-Type")
+ var enc Encoding
+ switch strings.ToLower(ct) {
+ case "text/plain", "text/plain; charset=iso-8859-1", "text/plain; charset=latin1":
+ enc = ISO_8859_1
+ case "", "text/plain; charset=utf-8":
+ enc = UTF8
+ default:
+ return nil, fmt.Errorf("properties: invalid content type %s", ct)
+ }
+
+ buf.WriteString(convert(body, enc))
+ buf.WriteRune('\n')
+ }
+ return loadBuf(buf.Bytes(), UTF8)
}
func must(p *Properties, err error) *Properties {
diff --git a/load_test.go b/load_test.go
index 0983af6..f95b948 100644
--- a/load_test.go
+++ b/load_test.go
@@ -7,6 +7,8 @@
import (
"fmt"
"io/ioutil"
+ "net/http"
+ "net/http/httptest"
"os"
"strings"
@@ -73,6 +75,55 @@
assertKeyValues(c, "", p, "key", "value", "key2", "value2")
}
+func (s *LoadSuite) TestLoadURL(c *C) {
+ srv := testServer()
+ defer srv.Close()
+ p := MustLoadURL(srv.URL + "/a")
+ assertKeyValues(c, "", p, "key", "value")
+}
+
+func (s *LoadSuite) TestLoadURLs(c *C) {
+ srv := testServer()
+ defer srv.Close()
+ p := MustLoadURLs([]string{srv.URL + "/a", srv.URL + "/b"}, false)
+ assertKeyValues(c, "", p, "key", "value", "key2", "value2")
+}
+
+func (s *LoadSuite) TestLoadURLsAndFailMissing(c *C) {
+ srv := testServer()
+ defer srv.Close()
+ p, err := LoadURLs([]string{srv.URL + "/a", srv.URL + "/c"}, false)
+ c.Assert(p, IsNil)
+ c.Assert(err, ErrorMatches, ".*returned 404.*")
+}
+
+func (s *LoadSuite) TestLoadURLsAndIgnoreMissing(c *C) {
+ srv := testServer()
+ defer srv.Close()
+ p := MustLoadURLs([]string{srv.URL + "/a", srv.URL + "/b", srv.URL + "/c"}, true)
+ assertKeyValues(c, "", p, "key", "value", "key2", "value2")
+}
+
+func (s *LoadSuite) TestLoadURLEncoding(c *C) {
+ srv := testServer()
+ defer srv.Close()
+
+ uris := []string{"/none", "/utf8", "/plain", "/latin1", "/iso88591"}
+ for i, uri := range uris {
+ p := MustLoadURL(srv.URL + uri)
+ c.Assert(p.GetString("key", ""), Equals, "äöü", Commentf("%d", i))
+ }
+}
+
+func (s *LoadSuite) TestLoadURLFailInvalidEncoding(c *C) {
+ srv := testServer()
+ defer srv.Close()
+
+ p, err := LoadURL(srv.URL + "/json")
+ c.Assert(p, IsNil)
+ c.Assert(err, ErrorMatches, ".*invalid content type.*")
+}
+
func (s *LoadSuite) SetUpSuite(c *C) {
s.tempFiles = make([]string, 0)
}
@@ -118,3 +169,36 @@
return f.Name()
}
+
+func testServer() *httptest.Server {
+ return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ send := func(data []byte, contentType string) {
+ w.Header().Set("Content-Type", contentType)
+ w.Write(data)
+ }
+
+ utf8 := []byte("key=äöü")
+ iso88591 := []byte{0x6b, 0x65, 0x79, 0x3d, 0xe4, 0xf6, 0xfc} // key=äöü
+
+ switch r.RequestURI {
+ case "/a":
+ send([]byte("key=value"), "")
+ case "/b":
+ send([]byte("key2=value2"), "")
+ case "/none":
+ send(utf8, "")
+ case "/utf8":
+ send(utf8, "text/plain; charset=utf-8")
+ case "/json":
+ send(utf8, "application/json; charset=utf-8")
+ case "/plain":
+ send(iso88591, "text/plain")
+ case "/latin1":
+ send(iso88591, "text/plain; charset=latin1")
+ case "/iso88591":
+ send(iso88591, "text/plain; charset=iso-8859-1")
+ default:
+ w.WriteHeader(404)
+ }
+ }))
+}
diff --git a/properties.go b/properties.go
index 90e2955..884ef4e 100644
--- a/properties.go
+++ b/properties.go
@@ -26,7 +26,11 @@
// ErrorHandler is the function which handles failures of the MustXXX()
// functions. The default is LogFatalHandler.
-var ErrorHandler = LogFatalHandler
+var ErrorHandler ErrorHandlerFunc = LogFatalHandler
+
+type LogHandlerFunc func(fmt string, args ...interface{})
+
+var LogPrintf LogHandlerFunc = log.Printf
// LogFatalHandler handles the error by logging a fatal error and exiting.
func LogFatalHandler(err error) {