1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.shiro.config;
20
21 import org.apache.shiro.io.ResourceUtils;
22 import org.apache.shiro.util.StringUtils;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
25
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.InputStreamReader;
29 import java.io.Reader;
30 import java.io.UnsupportedEncodingException;
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.LinkedHashMap;
34 import java.util.Map;
35 import java.util.Scanner;
36 import java.util.Set;
37
38
39
40
41
42
43
44
45
46
47 public class Ini implements Map<String, Ini.Section> {
48
49 private static transient final Logger log = LoggerFactory.getLogger(Ini.class);
50
51 public static final String DEFAULT_SECTION_NAME = "";
52 public static final String DEFAULT_CHARSET_NAME = "UTF-8";
53
54 public static final String COMMENT_POUND = "#";
55 public static final String COMMENT_SEMICOLON = ";";
56 public static final String SECTION_PREFIX = "[";
57 public static final String SECTION_SUFFIX = "]";
58
59 protected static final char ESCAPE_TOKEN = '\\';
60
61 private final Map<String, Section> sections;
62
63
64
65
66 public Ini() {
67 this.sections = new LinkedHashMap<String, Section>();
68 }
69
70
71
72
73
74
75 public Ini(Ini defaults) {
76 this();
77 if (defaults == null) {
78 throw new NullPointerException("Defaults cannot be null.");
79 }
80 for (Section section : defaults.getSections()) {
81 Section copy = new Section(section);
82 this.sections.put(section.getName(), copy);
83 }
84 }
85
86
87
88
89
90
91
92
93 public boolean isEmpty() {
94 Collection<Section> sections = this.sections.values();
95 if (!sections.isEmpty()) {
96 for (Section section : sections) {
97 if (!section.isEmpty()) {
98 return false;
99 }
100 }
101 }
102 return true;
103 }
104
105
106
107
108
109
110
111
112 public Set<String> getSectionNames() {
113 return Collections.unmodifiableSet(sections.keySet());
114 }
115
116
117
118
119
120
121
122
123 public Collection<Section> getSections() {
124 return Collections.unmodifiableCollection(sections.values());
125 }
126
127
128
129
130
131
132
133 public Section getSection(String sectionName) {
134 String name = cleanName(sectionName);
135 return sections.get(name);
136 }
137
138
139
140
141
142
143
144 public Section addSection(String sectionName) {
145 String name = cleanName(sectionName);
146 Section section = getSection(name);
147 if (section == null) {
148 section = new Section(name);
149 this.sections.put(name, section);
150 }
151 return section;
152 }
153
154
155
156
157
158
159
160 public Section removeSection(String sectionName) {
161 String name = cleanName(sectionName);
162 return this.sections.remove(name);
163 }
164
165 private static String cleanName(String sectionName) {
166 String name = StringUtils.clean(sectionName);
167 if (name == null) {
168 log.trace("Specified name was null or empty. Defaulting to the default section (name = \"\")");
169 name = DEFAULT_SECTION_NAME;
170 }
171 return name;
172 }
173
174
175
176
177
178
179
180
181
182
183 public void setSectionProperty(String sectionName, String propertyName, String propertyValue) {
184 String name = cleanName(sectionName);
185 Section section = getSection(name);
186 if (section == null) {
187 section = addSection(name);
188 }
189 section.put(propertyName, propertyValue);
190 }
191
192
193
194
195
196
197
198
199 public String getSectionProperty(String sectionName, String propertyName) {
200 Section section = getSection(sectionName);
201 return section != null ? section.get(propertyName) : null;
202 }
203
204
205
206
207
208
209
210
211
212
213
214 public String getSectionProperty(String sectionName, String propertyName, String defaultValue) {
215 String value = getSectionProperty(sectionName, propertyName);
216 return value != null ? value : defaultValue;
217 }
218
219
220
221
222
223
224
225
226
227
228 public static Ini fromResourcePath(String resourcePath) throws ConfigurationException {
229 if (!StringUtils.hasLength(resourcePath)) {
230 throw new IllegalArgumentException("Resource Path argument cannot be null or empty.");
231 }
232 Ini ini = new Ini();
233 ini.loadFromPath(resourcePath);
234 return ini;
235 }
236
237
238
239
240
241
242
243
244
245 public void loadFromPath(String resourcePath) throws ConfigurationException {
246 InputStream is;
247 try {
248 is = ResourceUtils.getInputStreamForPath(resourcePath);
249 } catch (IOException e) {
250 throw new ConfigurationException(e);
251 }
252 load(is);
253 }
254
255
256
257
258
259
260
261 public void load(String iniConfig) throws ConfigurationException {
262 load(new Scanner(iniConfig));
263 }
264
265
266
267
268
269
270
271
272
273 public void load(InputStream is) throws ConfigurationException {
274 if (is == null) {
275 throw new NullPointerException("InputStream argument cannot be null.");
276 }
277 InputStreamReader isr;
278 try {
279 isr = new InputStreamReader(is, DEFAULT_CHARSET_NAME);
280 } catch (UnsupportedEncodingException e) {
281 throw new ConfigurationException(e);
282 }
283 load(isr);
284 }
285
286
287
288
289
290
291
292 public void load(Reader reader) {
293 Scanner scanner = new Scanner(reader);
294 try {
295 load(scanner);
296 } finally {
297 try {
298 scanner.close();
299 } catch (Exception e) {
300 log.debug("Unable to cleanly close the InputStream scanner. Non-critical - ignoring.", e);
301 }
302 }
303 }
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343 public void merge(Map<String, Section> m) {
344
345 if (m != null) {
346 for (Entry<String, Section> entry : m.entrySet()) {
347 Section section = this.getSection(entry.getKey());
348 if (section == null) {
349 section = addSection(entry.getKey());
350 }
351 section.putAll(entry.getValue());
352 }
353 }
354 }
355
356 private void addSection(String name, StringBuilder content) {
357 if (content.length() > 0) {
358 String contentString = content.toString();
359 String cleaned = StringUtils.clean(contentString);
360 if (cleaned != null) {
361 Section section = new Section(name, contentString);
362 if (!section.isEmpty()) {
363 sections.put(name, section);
364 }
365 }
366 }
367 }
368
369
370
371
372
373
374
375 public void load(Scanner scanner) {
376
377 String sectionName = DEFAULT_SECTION_NAME;
378 StringBuilder sectionContent = new StringBuilder();
379
380 while (scanner.hasNextLine()) {
381
382 String rawLine = scanner.nextLine();
383 String line = StringUtils.clean(rawLine);
384
385 if (line == null || line.startsWith(COMMENT_POUND) || line.startsWith(COMMENT_SEMICOLON)) {
386
387 continue;
388 }
389
390 String newSectionName = getSectionName(line);
391 if (newSectionName != null) {
392
393 addSection(sectionName, sectionContent);
394
395
396 sectionContent = new StringBuilder();
397
398 sectionName = newSectionName;
399
400 if (log.isDebugEnabled()) {
401 log.debug("Parsing " + SECTION_PREFIX + sectionName + SECTION_SUFFIX);
402 }
403 } else {
404
405 sectionContent.append(rawLine).append("\n");
406 }
407 }
408
409
410 addSection(sectionName, sectionContent);
411 }
412
413 protected static boolean isSectionHeader(String line) {
414 String s = StringUtils.clean(line);
415 return s != null && s.startsWith(SECTION_PREFIX) && s.endsWith(SECTION_SUFFIX);
416 }
417
418 protected static String getSectionName(String line) {
419 String s = StringUtils.clean(line);
420 if (isSectionHeader(s)) {
421 return cleanName(s.substring(1, s.length() - 1));
422 }
423 return null;
424 }
425
426 public boolean equals(Object obj) {
427 if (obj instanceof Ini) {
428 Ini ini = (Ini) obj;
429 return this.sections.equals(ini.sections);
430 }
431 return false;
432 }
433
434 @Override
435 public int hashCode() {
436 return this.sections.hashCode();
437 }
438
439 public String toString() {
440 if (this.sections == null || this.sections.isEmpty()) {
441 return "<empty INI>";
442 } else {
443 StringBuilder sb = new StringBuilder("sections=");
444 int i = 0;
445 for (Ini.Section section : this.sections.values()) {
446 if (i > 0) {
447 sb.append(",");
448 }
449 sb.append(section.toString());
450 i++;
451 }
452 return sb.toString();
453 }
454 }
455
456 public int size() {
457 return this.sections.size();
458 }
459
460 public boolean containsKey(Object key) {
461 return this.sections.containsKey(key);
462 }
463
464 public boolean containsValue(Object value) {
465 return this.sections.containsValue(value);
466 }
467
468 public Section get(Object key) {
469 return this.sections.get(key);
470 }
471
472 public Section put(String key, Section value) {
473 return this.sections.put(key, value);
474 }
475
476 public Section remove(Object key) {
477 return this.sections.remove(key);
478 }
479
480 public void putAll(Map<? extends String, ? extends Section> m) {
481 this.sections.putAll(m);
482 }
483
484 public void clear() {
485 this.sections.clear();
486 }
487
488 public Set<String> keySet() {
489 return Collections.unmodifiableSet(this.sections.keySet());
490 }
491
492 public Collection<Section> values() {
493 return Collections.unmodifiableCollection(this.sections.values());
494 }
495
496 public Set<Entry<String, Section>> entrySet() {
497 return Collections.unmodifiableSet(this.sections.entrySet());
498 }
499
500
501
502
503
504 public static class Section implements Map<String, String> {
505 private final String name;
506 private final Map<String, String> props;
507
508 private Section(String name) {
509 if (name == null) {
510 throw new NullPointerException("name");
511 }
512 this.name = name;
513 this.props = new LinkedHashMap<String, String>();
514 }
515
516 private Section(String name, String sectionContent) {
517 if (name == null) {
518 throw new NullPointerException("name");
519 }
520 this.name = name;
521 Map<String,String> props;
522 if (StringUtils.hasText(sectionContent) ) {
523 props = toMapProps(sectionContent);
524 } else {
525 props = new LinkedHashMap<String,String>();
526 }
527 if ( props != null ) {
528 this.props = props;
529 } else {
530 this.props = new LinkedHashMap<String,String>();
531 }
532 }
533
534 private Section(Section defaults) {
535 this(defaults.getName());
536 putAll(defaults.props);
537 }
538
539
540
541 protected static boolean isContinued(String line) {
542 if (!StringUtils.hasText(line)) {
543 return false;
544 }
545 int length = line.length();
546
547
548 int backslashCount = 0;
549 for (int i = length - 1; i > 0; i--) {
550 if (line.charAt(i) == ESCAPE_TOKEN) {
551 backslashCount++;
552 } else {
553 break;
554 }
555 }
556 return backslashCount % 2 != 0;
557 }
558
559 private static boolean isKeyValueSeparatorChar(char c) {
560 return Character.isWhitespace(c) || c == ':' || c == '=';
561 }
562
563 private static boolean isCharEscaped(CharSequence s, int index) {
564 return index > 0 && s.charAt(index - 1) == ESCAPE_TOKEN;
565 }
566
567
568 protected static String[] splitKeyValue(String keyValueLine) {
569 String line = StringUtils.clean(keyValueLine);
570 if (line == null) {
571 return null;
572 }
573 StringBuilder keyBuffer = new StringBuilder();
574 StringBuilder valueBuffer = new StringBuilder();
575
576 boolean buildingKey = true;
577
578 for (int i = 0; i < line.length(); i++) {
579 char c = line.charAt(i);
580
581 if (buildingKey) {
582 if (isKeyValueSeparatorChar(c) && !isCharEscaped(line, i)) {
583 buildingKey = false;
584 } else {
585 keyBuffer.append(c);
586 }
587 } else {
588 if (valueBuffer.length() == 0 && isKeyValueSeparatorChar(c) && !isCharEscaped(line, i)) {
589
590 } else {
591 valueBuffer.append(c);
592 }
593 }
594 }
595
596 String key = StringUtils.clean(keyBuffer.toString());
597 String value = StringUtils.clean(valueBuffer.toString());
598
599 if (key == null || value == null) {
600 String msg = "Line argument must contain a key and a value. Only one string token was found.";
601 throw new IllegalArgumentException(msg);
602 }
603
604 log.trace("Discovered key/value pair: {} = {}", key, value);
605
606 return new String[]{key, value};
607 }
608
609 private static Map<String, String> toMapProps(String content) {
610 Map<String, String> props = new LinkedHashMap<String, String>();
611 String line;
612 StringBuilder lineBuffer = new StringBuilder();
613 Scanner scanner = new Scanner(content);
614 while (scanner.hasNextLine()) {
615 line = StringUtils.clean(scanner.nextLine());
616 if (isContinued(line)) {
617
618 line = line.substring(0, line.length() - 1);
619 lineBuffer.append(line);
620 continue;
621 } else {
622 lineBuffer.append(line);
623 }
624 line = lineBuffer.toString();
625 lineBuffer = new StringBuilder();
626 String[] kvPair = splitKeyValue(line);
627 props.put(kvPair[0], kvPair[1]);
628 }
629
630 return props;
631 }
632
633 public String getName() {
634 return this.name;
635 }
636
637 public void clear() {
638 this.props.clear();
639 }
640
641 public boolean containsKey(Object key) {
642 return this.props.containsKey(key);
643 }
644
645 public boolean containsValue(Object value) {
646 return this.props.containsValue(value);
647 }
648
649 public Set<Entry<String, String>> entrySet() {
650 return this.props.entrySet();
651 }
652
653 public String get(Object key) {
654 return this.props.get(key);
655 }
656
657 public boolean isEmpty() {
658 return this.props.isEmpty();
659 }
660
661 public Set<String> keySet() {
662 return this.props.keySet();
663 }
664
665 public String put(String key, String value) {
666 return this.props.put(key, value);
667 }
668
669 public void putAll(Map<? extends String, ? extends String> m) {
670 this.props.putAll(m);
671 }
672
673 public String remove(Object key) {
674 return this.props.remove(key);
675 }
676
677 public int size() {
678 return this.props.size();
679 }
680
681 public Collection<String> values() {
682 return this.props.values();
683 }
684
685 public String toString() {
686 String name = getName();
687 if (DEFAULT_SECTION_NAME.equals(name)) {
688 return "<default>";
689 }
690 return name;
691 }
692
693 @Override
694 public boolean equals(Object obj) {
695 if (obj instanceof Section) {
696 Section other = (Section) obj;
697 return getName().equals(other.getName()) && this.props.equals(other.props);
698 }
699 return false;
700 }
701
702 @Override
703 public int hashCode() {
704 return this.name.hashCode() * 31 + this.props.hashCode();
705 }
706 }
707
708 }