/
jsonmerge.go
236 lines (164 loc) · 6.04 KB
/
jsonmerge.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
// Copyright 2016-2018 Granitic. All rights reserved.
// Use of this source code is governed by an Apache 2.0 license that can be found in the LICENSE file at the root of this project.
package config
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/graniticio/granitic/instance"
"github.com/graniticio/granitic/logging"
"io/ioutil"
"net/http"
"strings"
)
const jsonMergerComponentName string = instance.FrameworkPrefix + "JsonMerger"
// A ContentParser can take a []byte of some structured file type (e.g. YAML, JSON() and convert into a map[string]interface{} representation
type ContentParser interface {
ParseInto(data []byte, target interface{}) error
Extensions() []string
ContentTypes() []string
}
type JsonContentParser struct {
}
func (jcp *JsonContentParser) ParseInto(data []byte, target interface{}) error {
return json.Unmarshal(data, &target)
}
func (jcp *JsonContentParser) Extensions() []string {
return []string{"json"}
}
func (jcp *JsonContentParser) ContentTypes() []string {
return []string{"application/json", "application/x-javascript", "text/javascript", "text/x-javascript", "text/x-json"}
}
// NewJsonMerger creates a JsonMerger with a Logger
func NewJsonMergerWithManagedLogging(flm *logging.ComponentLoggerManager, cp ContentParser) *JsonMerger {
l := flm.CreateLogger(jsonMergerComponentName)
return NewJsonMergerWithDirectLogging(l, cp)
}
func NewJsonMergerWithDirectLogging(l logging.Logger, cp ContentParser) *JsonMerger {
jm := new(JsonMerger)
jm.Logger = l
jm.DefaultParser = cp
jm.parserByContent = make(map[string]ContentParser)
jm.parserByFile = make(map[string]ContentParser)
jm.RegisterContentParser(cp)
return jm
}
// A JsonMerger can merge a sequence of JSON configuration files (from a filesystem or HTTP URL) into a single
// view of configuration that will be used to configure Grantic's facilities and the user's IoC components. See the top
// of this page for a brief explanation of how merging works.
type JsonMerger struct {
// Logger used by Granitic framework components. Automatically injected.
Logger logging.Logger
// True if arrays should be joined when merging; false if the entire conetnts of the array should be overwritten.
MergeArrays bool
DefaultParser ContentParser
parserByFile map[string]ContentParser
parserByContent map[string]ContentParser
}
// LoadAndMergeConfig takes a list of file paths or URIs to JSON files and merges them into a single in-memory object representation.
// See the top of this page for a brief explanation of how merging works. Returns an error if a remote URI returned a 4xx or 5xx response code,
// a file or folder could not be accessed or if two files could not be merged dued to JSON parsing errors.
func (jm *JsonMerger) LoadAndMergeConfig(files []string) (map[string]interface{}, error) {
mergedConfig := make(map[string]interface{})
return jm.LoadAndMergeConfigWithBase(mergedConfig, files)
}
func (jm *JsonMerger) RegisterContentParser(cp ContentParser) {
for _, ct := range cp.ContentTypes() {
jm.parserByContent[strings.ToLower(ct)] = cp
}
for _, ext := range cp.Extensions() {
jm.parserByFile[strings.ToLower(ext)] = cp
}
}
func (jm *JsonMerger) LoadAndMergeConfigWithBase(config map[string]interface{}, files []string) (map[string]interface{}, error) {
var jsonData []byte
var err error
for _, fileName := range files {
var cp ContentParser
if isURL(fileName) {
//Read config from a remote URL
jm.Logger.LogTracef("Acessing URL %s", fileName)
jsonData, cp, err = jm.loadFromURL(fileName)
} else {
//Read config from a filesystem file
jm.Logger.LogTracef("Reading file %s", fileName)
ext := jm.extractExtension(fileName)
if jm.parserByFile[ext] != nil {
jm.Logger.LogTracef("Found ContentParser for extension %s", ext)
cp = jm.parserByFile[ext]
} else {
jm.Logger.LogTracef("Skipping file with unsupported extension %s", ext)
continue
}
jsonData, err = ioutil.ReadFile(fileName)
}
if err != nil {
m := fmt.Sprintf("Problem reading data from file/URL %s: %s", fileName, err)
return nil, errors.New(m)
}
var loadedConfig interface{}
err = cp.ParseInto(jsonData, &loadedConfig)
if err != nil {
m := fmt.Sprintf("Problem parsing data from a file or URL (%s) as JSON : %s", fileName, err)
return nil, errors.New(m)
}
additionalConfig := loadedConfig.(map[string]interface{})
config = jm.merge(config, additionalConfig)
}
return config, nil
}
func (jm *JsonMerger) extractExtension(path string) string {
c := strings.Split(path, ".")
if len(c) == 1 {
return ""
} else {
return strings.ToLower(c[len(c)-1])
}
}
func (jm *JsonMerger) loadFromURL(url string) ([]byte, ContentParser, error) {
r, err := http.Get(url)
if err != nil {
return nil, nil, err
}
cp := jm.DefaultParser
if ct := r.Header.Get("content-type"); ct != "" {
ct = strings.Split(ct, ";")[0]
ct = strings.TrimSpace(ct)
ct = strings.ToLower(ct)
if jm.parserByContent[ct] != nil {
jm.Logger.LogDebugf("Found content parser for %s", ct)
cp = jm.parserByContent[ct]
}
}
if r.StatusCode >= 400 {
m := fmt.Sprintf("HTTP %d", r.StatusCode)
return nil, nil, errors.New(m)
}
var b bytes.Buffer
b.ReadFrom(r.Body)
r.Body.Close()
return b.Bytes(), cp, nil
}
func (jm *JsonMerger) merge(base, additional map[string]interface{}) map[string]interface{} {
for key, value := range additional {
if existingEntry, ok := base[key]; ok {
existingEntryType := JsonType(existingEntry)
newEntryType := JsonType(value)
if existingEntryType == JsonMap && newEntryType == JsonMap {
jm.merge(existingEntry.(map[string]interface{}), value.(map[string]interface{}))
} else if jm.MergeArrays && existingEntryType == JsonArray && newEntryType == JsonArray {
base[key] = jm.mergeArrays(existingEntry.([]interface{}), value.([]interface{}))
} else {
base[key] = value
}
} else {
jm.Logger.LogTracef("Adding %s", key)
base[key] = value
}
}
return base
}
func (jm *JsonMerger) mergeArrays(a []interface{}, b []interface{}) []interface{} {
return append(a, b...)
}