View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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   * A class representing the <a href="http://en.wikipedia.org/wiki/INI_file">INI</a> text configuration format.
40   * <p/>
41   * An Ini instance is a map of {@link Ini.Section Section}s, keyed by section name.  Each
42   * {@code Section} is itself a map of {@code String} name/value pairs.  Name/value pairs are guaranteed to be unique
43   * within each {@code Section} only - not across the entire {@code Ini} instance.
44   *
45   * @since 1.0
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 = ""; //empty string means the first unnamed section
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       * Creates a new empty {@code Ini} instance.
65       */
66      public Ini() {
67          this.sections = new LinkedHashMap<String, Section>();
68      }
69  
70      /**
71       * Creates a new {@code Ini} instance with the specified defaults.
72       *
73       * @param defaults the default sections and/or key-value pairs to copy into the new instance.
74       */
75      public Ini="Ini" href="../../../../org/apache/shiro/config/Ini.html#Ini">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       * Returns {@code true} if no sections have been configured, or if there are sections, but the sections themselves
88       * are all empty, {@code false} otherwise.
89       *
90       * @return {@code true} if no sections have been configured, or if there are sections, but the sections themselves
91       *         are all empty, {@code false} otherwise.
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      * Returns the names of all sections managed by this {@code Ini} instance or an empty collection if there are
107      * no sections.
108      *
109      * @return the names of all sections managed by this {@code Ini} instance or an empty collection if there are
110      *         no sections.
111      */
112     public Set<String> getSectionNames() {
113         return Collections.unmodifiableSet(sections.keySet());
114     }
115 
116     /**
117      * Returns the sections managed by this {@code Ini} instance or an empty collection if there are
118      * no sections.
119      *
120      * @return the sections managed by this {@code Ini} instance or an empty collection if there are
121      *         no sections.
122      */
123     public Collection<Section> getSections() {
124         return Collections.unmodifiableCollection(sections.values());
125     }
126 
127     /**
128      * Returns the {@link Section} with the given name or {@code null} if no section with that name exists.
129      *
130      * @param sectionName the name of the section to retrieve.
131      * @return the {@link Section} with the given name or {@code null} if no section with that name exists.
132      */
133     public Section getSection(String sectionName) {
134         String name = cleanName(sectionName);
135         return sections.get(name);
136     }
137 
138     /**
139      * Ensures a section with the specified name exists, adding a new one if it does not yet exist.
140      *
141      * @param sectionName the name of the section to ensure existence
142      * @return the section created if it did not yet exist, or the existing Section that already existed.
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      * Removes the section with the specified name and returns it, or {@code null} if the section did not exist.
156      *
157      * @param sectionName the name of the section to remove.
158      * @return the section with the specified name or {@code null} if the section did not exist.
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      * Sets a name/value pair for the section with the given {@code sectionName}.  If the section does not yet exist,
176      * it will be created.  If the {@code sectionName} is null or empty, the name/value pair will be placed in the
177      * default (unnamed, empty string) section.
178      *
179      * @param sectionName   the name of the section to add the name/value pair
180      * @param propertyName  the name of the property to add
181      * @param propertyValue the property value
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      * Returns the value of the specified section property, or {@code null} if the section or property do not exist.
194      *
195      * @param sectionName  the name of the section to retrieve to acquire the property value
196      * @param propertyName the name of the section property for which to return the value
197      * @return the value of the specified section property, or {@code null} if the section or property do not exist.
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      * Returns the value of the specified section property, or the {@code defaultValue} if the section or
206      * property do not exist.
207      *
208      * @param sectionName  the name of the section to add the name/value pair
209      * @param propertyName the name of the property to add
210      * @param defaultValue the default value to return if the section or property do not exist.
211      * @return the value of the specified section property, or the {@code defaultValue} if the section or
212      *         property do not exist.
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      * Creates a new {@code Ini} instance loaded with the INI-formatted data in the resource at the given path.  The
221      * resource path may be any value interpretable by the
222      * {@link ResourceUtils#getInputStreamForPath(String) ResourceUtils.getInputStreamForPath} method.
223      *
224      * @param resourcePath the resource location of the INI data to load when creating the {@code Ini} instance.
225      * @return a new {@code Ini} instance loaded with the INI-formatted data in the resource at the given path.
226      * @throws ConfigurationException if the path cannot be loaded into an {@code Ini} instance.
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         Inig/Ini.html#Ini">Ini ini = new Ini();
233         ini.loadFromPath(resourcePath);
234         return ini;
235     }
236 
237     /**
238      * Loads data from the specified resource path into this current {@code Ini} instance.  The
239      * resource path may be any value interpretable by the
240      * {@link ResourceUtils#getInputStreamForPath(String) ResourceUtils.getInputStreamForPath} method.
241      *
242      * @param resourcePath the resource location of the INI data to load into this instance.
243      * @throws ConfigurationException if the path cannot be loaded
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      * Loads the specified raw INI-formatted text into this instance.
257      *
258      * @param iniConfig the raw INI-formatted text to load into this instance.
259      * @throws ConfigurationException if the text cannot be loaded
260      */
261     public void load(String iniConfig) throws ConfigurationException {
262         load(new Scanner(iniConfig));
263     }
264 
265     /**
266      * Loads the INI-formatted text backed by the given InputStream into this instance.  This implementation will
267      * close the input stream after it has finished loading.  It is expected that the stream's contents are
268      * UTF-8 encoded.
269      *
270      * @param is the {@code InputStream} from which to read the INI-formatted text
271      * @throws ConfigurationException if unable
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      * Loads the INI-formatted text backed by the given Reader into this instance.  This implementation will close the
288      * reader after it has finished loading.
289      *
290      * @param reader the {@code Reader} from which to read the INI-formatted text
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      * Merges the contents of <code>m</code>'s {@link Section} objects into self.
307      * This differs from {@link Ini#putAll(Map)}, in that each section is merged with the existing one.
308      * For example the following two ini blocks are merged and the result is the third<BR/>
309      * <p>
310      * Initial:
311      * <pre>
312      * <code>[section1]
313      * key1 = value1
314      *
315      * [section2]
316      * key2 = value2
317      * </code> </pre>
318      *
319      * To be merged:
320      * <pre>
321      * <code>[section1]
322      * foo = bar
323      *
324      * [section2]
325      * key2 = new value
326      * </code> </pre>
327      *
328      * Result:
329      * <pre>
330      * <code>[section1]
331      * key1 = value1
332      * foo = bar
333      *
334      * [section2]
335      * key2 = new value
336      * </code> </pre>
337      *
338      * </p>
339      *
340      * @param m map to be merged
341      * @since 1.4
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      * Loads the INI-formatted text backed by the given Scanner.  This implementation will close the
371      * scanner after it has finished loading.
372      *
373      * @param scanner the {@code Scanner} from which to read the INI-formatted text
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                 //skip empty lines and comments:
387                 continue;
388             }
389 
390             String newSectionName = getSectionName(line);
391             if (newSectionName != null) {
392                 //found a new section - convert the currently buffered one into a Section object
393                 addSection(sectionName, sectionContent);
394 
395                 //reset the buffer for the new section:
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                 //normal line - add it to the existing content buffer:
405                 sectionContent.append(rawLine).append("\n");
406             }
407         }
408 
409         //finish any remaining buffered content:
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             Inihref="../../../../org/apache/shiro/config/Ini.html#Ini">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      * An {@code Ini.Section} is String-key-to-String-value Map, identifiable by a
502      * {@link #getName() name} unique within an {@link Ini} instance.
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         //Protected to access in a test case - NOT considered part of Shiro's public API
540 
541         protected static boolean isContinued(String line) {
542             if (!StringUtils.hasText(line)) {
543                 return false;
544             }
545             int length = line.length();
546             //find the number of backslashes at the end of the line.  If an even number, the
547             //backslashes are considered escaped.  If an odd number, the line is considered continued on the next line
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) == ESCAPE_TOKEN;
565         }
566 
567         //Protected to access in a test case - NOT considered part of Shiro's public API
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; //we'll build the value next:
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;//now start building the value
584                     } else if (!isCharEscaped(line, i)){
585                         keyBuffer.append(c);
586                     }
587                 } else {
588                     if (valueBuffer.length() == 0 && isKeyValueSeparatorChar(c) && !isCharEscaped(line, i)) {
589                         //swallow the separator chars before we start building the value
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                     //strip off the last continuation backslash:
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 }