Load from files and URLs (#12)

* Load from files and URLs
* Add Merge() method
diff --git a/.gitignore b/.gitignore
index 7054822..e7081ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,5 @@
 *.sublime-workspace
 *.un~
 *.swp
+.idea/
+*.iml
diff --git a/load.go b/load.go
index 3915c73..4300fec 100644
--- a/load.go
+++ b/load.go
@@ -5,7 +5,6 @@
 package properties
 
 import (
-	"bytes"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -36,14 +35,14 @@
 
 // LoadFile reads a file into a Properties struct.
 func LoadFile(filename string, enc Encoding) (*Properties, error) {
-	return loadFiles([]string{filename}, enc, false)
+	return loadAll([]string{filename}, enc, false)
 }
 
 // LoadFiles reads multiple files in the given order into
 // a Properties struct. If 'ignoreMissing' is true then
 // non-existent files will not be reported as error.
 func LoadFiles(filenames []string, enc Encoding, ignoreMissing bool) (*Properties, error) {
-	return loadFiles(filenames, enc, ignoreMissing)
+	return loadAll(filenames, enc, ignoreMissing)
 }
 
 // LoadURL reads the content of the URL into a Properties struct.
@@ -55,7 +54,7 @@
 // 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)
+	return loadAll([]string{url}, UTF8, false)
 }
 
 // LoadURLs reads the content of multiple URLs in the given order into a
@@ -63,7 +62,15 @@
 // 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)
+	return loadAll(urls, UTF8, ignoreMissing)
+}
+
+// LoadAll reads the content of multiple URLs or files in the given order into a
+// Properties struct. If 'ignoreMissing' is true then a 404 status code or missing file will
+// not be reported as error. Encoding sets the encoding for files. For the URLs please see
+// LoadURL for the Content-Type header and the encoding.
+func LoadAll(names []string, enc Encoding, ignoreMissing bool) (*Properties, error) {
+	return loadAll(names, enc, ignoreMissing)
 }
 
 // MustLoadString reads an UTF8 string into a Properties struct and
@@ -98,6 +105,14 @@
 	return must(LoadURLs(urls, ignoreMissing))
 }
 
+// MustLoadAll reads the content of multiple URLs or files in the given order into a
+// Properties struct. If 'ignoreMissing' is true then a 404 status code or missing file will
+// not be reported as error. Encoding sets the encoding for files. For the URLs please see
+// LoadURL for the Content-Type header and the encoding. It panics on error.
+func MustLoadAll(names []string, enc Encoding, ignoreMissing bool) *Properties {
+	return must(LoadAll(names, enc, ignoreMissing))
+}
+
 func loadBuf(buf []byte, enc Encoding) (*Properties, error) {
 	p, err := parse(convert(buf, enc))
 	if err != nil {
@@ -106,66 +121,78 @@
 	return p, p.check()
 }
 
-func loadFiles(filenames []string, enc Encoding, ignoreMissing bool) (*Properties, error) {
-	var buf bytes.Buffer
-	for _, filename := range filenames {
-		f, err := expandFilename(filename)
+func loadAll(names []string, enc Encoding, ignoreMissing bool) (*Properties, error) {
+	result := NewProperties()
+	for _, name := range names {
+		n, err := expandName(name)
 		if err != nil {
 			return nil, err
 		}
-
-		data, err := ioutil.ReadFile(f)
+		var p *Properties
+		if strings.HasPrefix(n, "http://") || strings.HasPrefix(n, "https://") {
+			p, err = loadURL(n, ignoreMissing)
+		} else {
+			p, err = loadFile(n, enc, ignoreMissing)
+		}
 		if err != nil {
-			if ignoreMissing && os.IsNotExist(err) {
-				LogPrintf("properties: %s not found. skipping", filename)
-				continue
-			}
 			return nil, err
 		}
+		result.Merge(p)
 
-		// concatenate the buffers and add a new line in case
-		// the previous file didn't end with a new line
-		buf.Write(data)
-		buf.WriteRune('\n')
 	}
-	return loadBuf(buf.Bytes(), enc)
+	return result, result.check()
 }
 
-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)
+func loadFile(filename string, enc Encoding, ignoreMissing bool) (*Properties, error) {
+	data, err := ioutil.ReadFile(filename)
+	if err != nil {
+		if ignoreMissing && os.IsNotExist(err) {
+			LogPrintf("properties: %s not found. skipping", filename)
+			return NewProperties(), nil
 		}
-		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 nil, err
 	}
-	return loadBuf(buf.Bytes(), UTF8)
+	p, err := parse(convert(data, enc))
+	if err != nil {
+		return nil, err
+	}
+	return p, nil
+}
+
+func loadURL(url string, ignoreMissing bool) (*Properties, error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return nil, fmt.Errorf("properties: error fetching %q. %s", url, err)
+	}
+	if resp.StatusCode == 404 && ignoreMissing {
+		LogPrintf("properties: %s returned %d. skipping", url, resp.StatusCode)
+		return NewProperties(), nil
+	}
+	if resp.StatusCode != 200 {
+		return nil, fmt.Errorf("properties: %s returned %d", url, resp.StatusCode)
+	}
+	body, err := ioutil.ReadAll(resp.Body)
+	resp.Body.Close()
+	if err != nil {
+		return nil, fmt.Errorf("properties: %s error reading response. %s", url, 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)
+	}
+
+	p, err := parse(convert(body, enc))
+	if err != nil {
+		return nil, err
+	}
+	return p, nil
 }
 
 func must(p *Properties, err error) *Properties {
@@ -175,12 +202,12 @@
 	return p
 }
 
-// expandFilename expands ${ENV_VAR} expressions in a filename.
+// expandName expands ${ENV_VAR} expressions in a name.
 // If the environment variable does not exist then it will be replaced
 // with an empty string. Malformed expressions like "${ENV_VAR" will
 // be reported as error.
-func expandFilename(filename string) (string, error) {
-	return expand(filename, make(map[string]bool), "${", "}", make(map[string]string))
+func expandName(name string) (string, error) {
+	return expand(name, make(map[string]bool), "${", "}", make(map[string]string))
 }
 
 // Interprets a byte buffer either as an ISO-8859-1 or UTF-8 encoded string.
diff --git a/load_test.go b/load_test.go
index f95b948..f56adb2 100644
--- a/load_test.go
+++ b/load_test.go
@@ -124,6 +124,16 @@
 	c.Assert(err, ErrorMatches, ".*invalid content type.*")
 }
 
+func (s *LoadSuite) TestLoadAll(c *C) {
+	filename := s.makeFile(c, "key=value")
+	filename2 := s.makeFile(c, "key2=value3")
+	filename3 := s.makeFile(c, "key=value4")
+	srv := testServer()
+	defer srv.Close()
+	p := MustLoadAll([]string{filename, filename2, srv.URL + "/a", srv.URL + "/b", filename3}, UTF8, false)
+	assertKeyValues(c, "", p, "key", "value4", "key2", "value2")
+}
+
 func (s *LoadSuite) SetUpSuite(c *C) {
 	s.tempFiles = make([]string, 0)
 }
diff --git a/properties.go b/properties.go
index 884ef4e..e7e0104 100644
--- a/properties.go
+++ b/properties.go
@@ -630,6 +630,26 @@
 	p.k = newKeys
 }
 
+// Merge merges properties, comments and keys from other *Properties into p
+func (p *Properties) Merge(other *Properties) {
+	for k,v := range other.m {
+		p.m[k] = v
+	}
+	for k,v := range other.c {
+		p.c[k] = v
+	}
+
+	outer:
+	for _, otherKey := range other.k {
+		for _, key := range p.k {
+			if otherKey == key {
+				continue outer
+			}
+		}
+		p.k = append(p.k, otherKey)
+	}
+}
+
 // ----------------------------------------------------------------------------
 
 // check expands all values and returns an error if a circular reference or
diff --git a/properties_test.go b/properties_test.go
index c0af16e..0d61be0 100644
--- a/properties_test.go
+++ b/properties_test.go
@@ -842,6 +842,20 @@
 	c.Check(len(p.k), Equals, 1)
 }
 
+func (s *TestSuite) TestMerge(c *C) {
+	input1 := "#comment\nkey=value\nkey2=value2"
+	input2 := "#another comment\nkey=another value\nkey3=value3"
+	p1, err := parse(input1)
+	c.Assert(err, IsNil)
+	p2, err := parse(input2)
+	p1.Merge(p2)
+	c.Check(len(p1.m), Equals, 3)
+	c.Check(len(p1.c), Equals, 1)
+	c.Check(len(p1.k), Equals, 3)
+	c.Check(p1.MustGet("key"), Equals, "another value")
+	c.Check(p1.GetComment("key"), Equals, "another comment")
+}
+
 // ----------------------------------------------------------------------------
 
 // tests all combinations of delimiters, leading and/or trailing whitespace and newlines.