1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.springframework.beans.factory.config;
18
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.util.AbstractMap;
22 import java.util.Arrays;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.LinkedHashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Map.Entry;
29 import java.util.Properties;
30 import java.util.Set;
31
32 import org.apache.commons.logging.Log;
33 import org.apache.commons.logging.LogFactory;
34 import org.yaml.snakeyaml.Yaml;
35 import org.yaml.snakeyaml.constructor.Constructor;
36 import org.yaml.snakeyaml.nodes.MappingNode;
37 import org.yaml.snakeyaml.parser.ParserException;
38
39 import org.springframework.core.io.Resource;
40 import org.springframework.util.Assert;
41 import org.springframework.util.StringUtils;
42
43
44
45
46
47
48
49 public abstract class YamlProcessor {
50
51 private final Log logger = LogFactory.getLog(getClass());
52
53 private ResolutionMethod resolutionMethod = ResolutionMethod.OVERRIDE;
54
55 private Resource[] resources = new Resource[0];
56
57 private List<DocumentMatcher> documentMatchers = Collections.emptyList();
58
59 private boolean matchDefault = true;
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 public void setDocumentMatchers(DocumentMatcher... matchers) {
91 this.documentMatchers = Arrays.asList(matchers);
92 }
93
94
95
96
97
98
99
100 public void setMatchDefault(boolean matchDefault) {
101 this.matchDefault = matchDefault;
102 }
103
104
105
106
107
108
109
110
111 public void setResolutionMethod(ResolutionMethod resolutionMethod) {
112 Assert.notNull(resolutionMethod, "ResolutionMethod must not be null");
113 this.resolutionMethod = resolutionMethod;
114 }
115
116
117
118
119
120 public void setResources(Resource... resources) {
121 this.resources = resources;
122 }
123
124
125
126
127
128
129
130
131
132
133
134
135 protected void process(MatchCallback callback) {
136 Yaml yaml = createYaml();
137 for (Resource resource : this.resources) {
138 boolean found = process(callback, yaml, resource);
139 if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND && found) {
140 return;
141 }
142 }
143 }
144
145
146
147
148 protected Yaml createYaml() {
149 return new Yaml(new StrictMapAppenderConstructor());
150 }
151
152 private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
153 int count = 0;
154 try {
155 if (this.logger.isDebugEnabled()) {
156 this.logger.debug("Loading from YAML: " + resource);
157 }
158 InputStream stream = resource.getInputStream();
159 try {
160 for (Object object : yaml.loadAll(stream)) {
161 if (object != null && process(asMap(object), callback)) {
162 count++;
163 if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND) {
164 break;
165 }
166 }
167 }
168 if (this.logger.isDebugEnabled()) {
169 this.logger.debug("Loaded " + count + " document" + (count > 1 ? "s" : "") +
170 " from YAML resource: " + resource);
171 }
172 }
173 finally {
174 stream.close();
175 }
176 }
177 catch (IOException ex) {
178 handleProcessError(resource, ex);
179 }
180 return (count > 0);
181 }
182
183 private void handleProcessError(Resource resource, IOException ex) {
184 if (this.resolutionMethod != ResolutionMethod.FIRST_FOUND &&
185 this.resolutionMethod != ResolutionMethod.OVERRIDE_AND_IGNORE) {
186 throw new IllegalStateException(ex);
187 }
188 if (this.logger.isWarnEnabled()) {
189 this.logger.warn("Could not load map from " + resource + ": " + ex.getMessage());
190 }
191 }
192
193 @SuppressWarnings("unchecked")
194 private Map<String, Object> asMap(Object object) {
195
196 Map<String, Object> result = new LinkedHashMap<String, Object>();
197 if (!(object instanceof Map)) {
198
199 result.put("document", object);
200 return result;
201 }
202
203 Map<Object, Object> map = (Map<Object, Object>) object;
204 for (Entry<Object, Object> entry : map.entrySet()) {
205 Object value = entry.getValue();
206 if (value instanceof Map) {
207 value = asMap(value);
208 }
209 Object key = entry.getKey();
210 if (key instanceof CharSequence) {
211 result.put(key.toString(), value);
212 }
213 else {
214
215 result.put("[" + key.toString() + "]", value);
216 }
217 }
218 return result;
219 }
220
221 private boolean process(Map<String, Object> map, MatchCallback callback) {
222 Properties properties = new Properties();
223 properties.putAll(getFlattenedMap(map));
224
225 if (this.documentMatchers.isEmpty()) {
226 if (this.logger.isDebugEnabled()) {
227 this.logger.debug("Merging document (no matchers set)" + map);
228 }
229 callback.process(properties, map);
230 return true;
231 }
232
233 MatchStatus result = MatchStatus.ABSTAIN;
234 for (DocumentMatcher matcher : this.documentMatchers) {
235 MatchStatus match = matcher.matches(properties);
236 result = MatchStatus.getMostSpecific(match, result);
237 if (match == MatchStatus.FOUND) {
238 if (this.logger.isDebugEnabled()) {
239 this.logger.debug("Matched document with document matcher: " + properties);
240 }
241 callback.process(properties, map);
242 return true;
243 }
244 }
245
246 if (result == MatchStatus.ABSTAIN && this.matchDefault) {
247 if (this.logger.isDebugEnabled()) {
248 this.logger.debug("Matched document with default matcher: " + map);
249 }
250 callback.process(properties, map);
251 return true;
252 }
253
254 this.logger.debug("Unmatched document");
255 return false;
256 }
257
258
259
260
261
262
263
264
265
266
267 protected final Map<String, Object> getFlattenedMap(Map<String, Object> source) {
268 Map<String, Object> result = new LinkedHashMap<String, Object>();
269 buildFlattenedMap(result, source, null);
270 return result;
271 }
272
273 private void buildFlattenedMap(Map<String, Object> result, Map<String, Object> source, String path) {
274 for (Entry<String, Object> entry : source.entrySet()) {
275 String key = entry.getKey();
276 if (StringUtils.hasText(path)) {
277 if (key.startsWith("[")) {
278 key = path + key;
279 }
280 else {
281 key = path + "." + key;
282 }
283 }
284 Object value = entry.getValue();
285 if (value instanceof String) {
286 result.put(key, value);
287 }
288 else if (value instanceof Map) {
289
290 @SuppressWarnings("unchecked")
291 Map<String, Object> map = (Map<String, Object>) value;
292 buildFlattenedMap(result, map, key);
293 }
294 else if (value instanceof Collection) {
295
296 @SuppressWarnings("unchecked")
297 Collection<Object> collection = (Collection<Object>) value;
298 int count = 0;
299 for (Object object : collection) {
300 buildFlattenedMap(result,
301 Collections.singletonMap("[" + (count++) + "]", object), key);
302 }
303 }
304 else {
305 result.put(key, value == null ? "" : value);
306 }
307 }
308 }
309
310
311
312
313
314 public interface MatchCallback {
315
316
317
318
319
320
321 void process(Properties properties, Map<String, Object> map);
322 }
323
324
325
326
327
328 public interface DocumentMatcher {
329
330
331
332
333
334
335 MatchStatus matches(Properties properties);
336 }
337
338
339
340
341
342 public enum MatchStatus {
343
344
345
346
347 FOUND,
348
349
350
351
352 NOT_FOUND,
353
354
355
356
357 ABSTAIN;
358
359
360
361
362 public static MatchStatus getMostSpecific(MatchStatus a, MatchStatus b) {
363 return (a.ordinal() < b.ordinal() ? a : b);
364 }
365 }
366
367
368
369
370
371 public enum ResolutionMethod {
372
373
374
375
376 OVERRIDE,
377
378
379
380
381 OVERRIDE_AND_IGNORE,
382
383
384
385
386 FIRST_FOUND
387 }
388
389
390
391
392
393 protected static class StrictMapAppenderConstructor extends Constructor {
394
395 public StrictMapAppenderConstructor() {
396 super();
397 }
398
399 @Override
400 protected Map<Object, Object> constructMapping(MappingNode node) {
401 try {
402 return super.constructMapping(node);
403 } catch (IllegalStateException e) {
404 throw new ParserException("while parsing MappingNode",
405 node.getStartMark(), e.getMessage(), node.getEndMark());
406 }
407 }
408
409 @Override
410 protected Map<Object, Object> createDefaultMap() {
411 final Map<Object, Object> delegate = super.createDefaultMap();
412 return new AbstractMap<Object, Object>() {
413 @Override
414 public Object put(Object key, Object value) {
415 if (delegate.containsKey(key)) {
416 throw new IllegalStateException("duplicate key: " + key);
417 }
418 return delegate.put(key, value);
419 }
420 @Override
421 public Set<Entry<Object, Object>> entrySet() {
422 return delegate.entrySet();
423 }
424 };
425 }
426
427 }
428
429 }