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.CollectionUtils;
23  import org.apache.shiro.util.StringUtils;
24  import org.slf4j.Logger;
25  import org.slf4j.LoggerFactory;
26  
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.InputStreamReader;
30  import java.io.Reader;
31  import java.io.UnsupportedEncodingException;
32  import java.util.Collection;
33  import java.util.Collections;
34  import java.util.LinkedHashMap;
35  import java.util.Map;
36  import java.util.Scanner;
37  import java.util.Set;
38  
39  /**
40   * A class representing the <a href="http://en.wikipedia.org/wiki/INI_file">INI</a> text configuration format.
41   * <p/>
42   * An Ini instance is a map of {@link Ini.Section Section}s, keyed by section name.  Each
43   * {@code Section} is itself a map of {@code String} name/value pairs.  Name/value pairs are guaranteed to be unique
44   * within each {@code Section} only - not across the entire {@code Ini} instance.
45   *
46   * @since 1.0
47   */
48  public class Ini implements Map<String, Ini.Section> {
49  
50      private static transient final Logger log = LoggerFactory.getLogger(Ini.class);
51  
52      public static final String DEFAULT_SECTION_NAME = ""; //empty string means the first unnamed section
53      public static final String DEFAULT_CHARSET_NAME = "UTF-8";
54  
55      public static final String COMMENT_POUND = "#";
56      public static final String COMMENT_SEMICOLON = ";";
57      public static final String SECTION_PREFIX = "[";
58      public static final String SECTION_SUFFIX = "]";
59  
60      protected static final char ESCAPE_TOKEN = '\\';
61  
62      private final Map<String, Section> sections;
63  
64      /**
65       * Creates a new empty {@code Ini} instance.
66       */
67      public Ini() {
68          this.sections = new LinkedHashMap<String, Section>();
69      }
70  
71      /**
72       * Creates a new {@code Ini} instance with the specified defaults.
73       *
74       * @param defaults the default sections and/or key-value pairs to copy into the new instance.
75       */
76      public Ini(Ini defaults) {
77          this();
78          if (defaults == null) {
79              throw new NullPointerException("Defaults cannot be null.");
80          }
81          for (Section section : defaults.getSections()) {
82              Section copy = new Section(section);
83              this.sections.put(section.getName(), copy);
84          }
85      }
86  
87      /**
88       * Returns {@code true} if no sections have been configured, or if there are sections, but the sections themselves
89       * are all empty, {@code false} otherwise.
90       *
91       * @return {@code true} if no sections have been configured, or if there are sections, but the sections themselves
92       *         are all empty, {@code false} otherwise.
93       */
94      public boolean isEmpty() {
95          Collection<Section> sections = this.sections.values();
96          if (!sections.isEmpty()) {
97              for (Section section : sections) {
98                  if (!section.isEmpty()) {
99                      return false;
100                 }
101             }
102         }
103         return true;
104     }
105 
106     /**
107      * Returns the names of all sections managed by this {@code Ini} instance or an empty collection if there are
108      * no sections.
109      *
110      * @return the names of all sections managed by this {@code Ini} instance or an empty collection if there are
111      *         no sections.
112      */
113     public Set<String> getSectionNames() {
114         return Collections.unmodifiableSet(sections.keySet());
115     }
116 
117     /**
118      * Returns the sections managed by this {@code Ini} instance or an empty collection if there are
119      * no sections.
120      *
121      * @return the sections managed by this {@code Ini} instance or an empty collection if there are
122      *         no sections.
123      */
124     public Collection<Section> getSections() {
125         return Collections.unmodifiableCollection(sections.values());
126     }
127 
128     /**
129      * Returns the {@link Section} with the given name or {@code null} if no section with that name exists.
130      *
131      * @param sectionName the name of the section to retrieve.
132      * @return the {@link Section} with the given name or {@code null} if no section with that name exists.
133      */
134     public Section getSection(String sectionName) {
135         String name = cleanName(sectionName);
136         return sections.get(name);
137     }
138 
139     /**
140      * Ensures a section with the specified name exists, adding a new one if it does not yet exist.
141      *
142      * @param sectionName the name of the section to ensure existence
143      * @return the section created if it did not yet exist, or the existing Section that already existed.
144      */
145     public Section addSection(String sectionName) {
146         String name = cleanName(sectionName);
147         Section section = getSection(name);
148         if (section == null) {
149             section = new Section(name);
150             this.sections.put(name, section);
151         }
152         return section;
153     }
154 
155     /**
156      * Removes the section with the specified name and returns it, or {@code null} if the section did not exist.
157      *
158      * @param sectionName the name of the section to remove.
159      * @return the section with the specified name or {@code null} if the section did not exist.
160      */
161     public Section removeSection(String sectionName) {
162         String name = cleanName(sectionName);
163         return this.sections.remove(name);
164     }
165 
166     private static String cleanName(String sectionName) {
167         String name = StringUtils.clean(sectionName);
168         if (name == null) {
169             log.trace("Specified name was null or empty.  Defaulting to the default section (name = \"\")");
170             name = DEFAULT_SECTION_NAME;
171         }
172         return name;
173     }
174 
175     /**
176      * Sets a name/value pair for the section with the given {@code sectionName}.  If the section does not yet exist,
177      * it will be created.  If the {@code sectionName} is null or empty, the name/value pair will be placed in the
178      * default (unnamed, empty string) section.
179      *
180      * @param sectionName   the name of the section to add the name/value pair
181      * @param propertyName  the name of the property to add
182      * @param propertyValue the property value
183      */
184     public void setSectionProperty(String sectionName, String propertyName, String propertyValue) {
185         String name = cleanName(sectionName);
186         Section section = getSection(name);
187         if (section == null) {
188             section = addSection(name);
189         }
190         section.put(propertyName, propertyValue);
191     }
192 
193     /**
194      * Returns the value of the specified section property, or {@code null} if the section or property do not exist.
195      *
196      * @param sectionName  the name of the section to retrieve to acquire the property value
197      * @param propertyName the name of the section property for which to return the value
198      * @return the value of the specified section property, or {@code null} if the section or property do not exist.
199      */
200     public String getSectionProperty(String sectionName, String propertyName) {
201         Section section = getSection(sectionName);
202         return section != null ? section.get(propertyName) : null;
203     }
204 
205     /**
206      * Returns the value of the specified section property, or the {@code defaultValue} if the section or
207      * property do not exist.
208      *
209      * @param sectionName  the name of the section to add the name/value pair
210      * @param propertyName the name of the property to add
211      * @param defaultValue the default value to return if the section or property do not exist.
212      * @return the value of the specified section property, or the {@code defaultValue} if the section or
213      *         property do not exist.
214      */
215     public String getSectionProperty(String sectionName, String propertyName, String defaultValue) {
216         String value = getSectionProperty(sectionName, propertyName);
217         return value != null ? value : defaultValue;
218     }
219 
220     /**
221      * Creates a new {@code Ini} instance loaded with the INI-formatted data in the resource at the given path.  The
222      * resource path may be any value interpretable by the
223      * {@link ResourceUtils#getInputStreamForPath(String) ResourceUtils.getInputStreamForPath} method.
224      *
225      * @param resourcePath the resource location of the INI data to load when creating the {@code Ini} instance.
226      * @return a new {@code Ini} instance loaded with the INI-formatted data in the resource at the given path.
227      * @throws ConfigurationException if the path cannot be loaded into an {@code Ini} instance.
228      */
229     public static Ini fromResourcePath(String resourcePath) throws ConfigurationException {
230         if (!StringUtils.hasLength(resourcePath)) {
231             throw new IllegalArgumentException("Resource Path argument cannot be null or empty.");
232         }
233         Ini ini = new Ini();
234         ini.loadFromPath(resourcePath);
235         return ini;
236     }
237 
238     /**
239      * Loads data from the specified resource path into this current {@code Ini} instance.  The
240      * resource path may be any value interpretable by the
241      * {@link ResourceUtils#getInputStreamForPath(String) ResourceUtils.getInputStreamForPath} method.
242      *
243      * @param resourcePath the resource location of the INI data to load into this instance.
244      * @throws ConfigurationException if the path cannot be loaded
245      */
246     public void loadFromPath(String resourcePath) throws ConfigurationException {
247         InputStream is;
248         try {
249             is = ResourceUtils.getInputStreamForPath(resourcePath);
250         } catch (IOException e) {
251             throw new ConfigurationException(e);
252         }
253         load(is);
254     }
255 
256     /**
257      * Loads the specified raw INI-formatted text into this instance.
258      *
259      * @param iniConfig the raw INI-formatted text to load into this instance.
260      * @throws ConfigurationException if the text cannot be loaded
261      */
262     public void load(String iniConfig) throws ConfigurationException {
263         load(new Scanner(iniConfig));
264     }
265 
266     /**
267      * Loads the INI-formatted text backed by the given InputStream into this instance.  This implementation will
268      * close the input stream after it has finished loading.  It is expected that the stream's contents are
269      * UTF-8 encoded.
270      *
271      * @param is the {@code InputStream} from which to read the INI-formatted text
272      * @throws ConfigurationException if unable
273      */
274     public void load(InputStream is) throws ConfigurationException {
275         if (is == null) {
276             throw new NullPointerException("InputStream argument cannot be null.");
277         }
278         InputStreamReader isr;
279         try {
280             isr = new InputStreamReader(is, DEFAULT_CHARSET_NAME);
281         } catch (UnsupportedEncodingException e) {
282             throw new ConfigurationException(e);
283         }
284         load(isr);
285     }
286 
287     /**
288      * Loads the INI-formatted text backed by the given Reader into this instance.  This implementation will close the
289      * reader after it has finished loading.
290      *
291      * @param reader the {@code Reader} from which to read the INI-formatted text
292      */
293     public void load(Reader reader) {
294         Scanner scanner = new Scanner(reader);
295         try {
296             load(scanner);
297         } finally {
298             try {
299                 scanner.close();
300             } catch (Exception e) {
301                 log.debug("Unable to cleanly close the InputStream scanner.  Non-critical - ignoring.", e);
302             }
303         }
304     }
305 
306     private void addSection(String name, StringBuilder content) {
307         if (content.length() > 0) {
308             String contentString = content.toString();
309             String cleaned = StringUtils.clean(contentString);
310             if (cleaned != null) {
311                 Section section = new Section(name, contentString);
312                 if (!section.isEmpty()) {
313                     sections.put(name, section);
314                 }
315             }
316         }
317     }
318 
319     /**
320      * Loads the INI-formatted text backed by the given Scanner.  This implementation will close the
321      * scanner after it has finished loading.
322      *
323      * @param scanner the {@code Scanner} from which to read the INI-formatted text
324      */
325     public void load(Scanner scanner) {
326 
327         String sectionName = DEFAULT_SECTION_NAME;
328         StringBuilder sectionContent = new StringBuilder();
329 
330         while (scanner.hasNextLine()) {
331 
332             String rawLine = scanner.nextLine();
333             String line = StringUtils.clean(rawLine);
334 
335             if (line == null || line.startsWith(COMMENT_POUND) || line.startsWith(COMMENT_SEMICOLON)) {
336                 //skip empty lines and comments:
337                 continue;
338             }
339 
340             String newSectionName = getSectionName(line);
341             if (newSectionName != null) {
342                 //found a new section - convert the currently buffered one into a Section object
343                 addSection(sectionName, sectionContent);
344 
345                 //reset the buffer for the new section:
346                 sectionContent = new StringBuilder();
347 
348                 sectionName = newSectionName;
349 
350                 if (log.isDebugEnabled()) {
351                     log.debug("Parsing " + SECTION_PREFIX + sectionName + SECTION_SUFFIX);
352                 }
353             } else {
354                 //normal line - add it to the existing content buffer:
355                 sectionContent.append(rawLine).append("\n");
356             }
357         }
358 
359         //finish any remaining buffered content:
360         addSection(sectionName, sectionContent);
361     }
362 
363     protected static boolean isSectionHeader(String line) {
364         String s = StringUtils.clean(line);
365         return s != null && s.startsWith(SECTION_PREFIX) && s.endsWith(SECTION_SUFFIX);
366     }
367 
368     protected static String getSectionName(String line) {
369         String s = StringUtils.clean(line);
370         if (isSectionHeader(s)) {
371             return cleanName(s.substring(1, s.length() - 1));
372         }
373         return null;
374     }
375 
376     public boolean equals(Object obj) {
377         if (obj instanceof Ini) {
378             Ini ini = (Ini) obj;
379             return this.sections.equals(ini.sections);
380         }
381         return false;
382     }
383 
384     @Override
385     public int hashCode() {
386         return this.sections.hashCode();
387     }
388 
389     public String toString() {
390         if (CollectionUtils.isEmpty(this.sections)) {
391             return "<empty INI>";
392         } else {
393             StringBuilder sb = new StringBuilder("sections=");
394             int i = 0;
395             for (Ini.Section section : this.sections.values()) {
396                 if (i > 0) {
397                     sb.append(",");
398                 }
399                 sb.append(section.toString());
400                 i++;
401             }
402             return sb.toString();
403         }
404     }
405 
406     public int size() {
407         return this.sections.size();
408     }
409 
410     public boolean containsKey(Object key) {
411         return this.sections.containsKey(key);
412     }
413 
414     public boolean containsValue(Object value) {
415         return this.sections.containsValue(value);
416     }
417 
418     public Section get(Object key) {
419         return this.sections.get(key);
420     }
421 
422     public Section put(String key, Section value) {
423         return this.sections.put(key, value);
424     }
425 
426     public Section remove(Object key) {
427         return this.sections.remove(key);
428     }
429 
430     public void putAll(Map<? extends String, ? extends Section> m) {
431         this.sections.putAll(m);
432     }
433 
434     public void clear() {
435         this.sections.clear();
436     }
437 
438     public Set<String> keySet() {
439         return Collections.unmodifiableSet(this.sections.keySet());
440     }
441 
442     public Collection<Section> values() {
443         return Collections.unmodifiableCollection(this.sections.values());
444     }
445 
446     public Set<Entry<String, Section>> entrySet() {
447         return Collections.unmodifiableSet(this.sections.entrySet());
448     }
449 
450     /**
451      * An {@code Ini.Section} is String-key-to-String-value Map, identifiable by a
452      * {@link #getName() name} unique within an {@link Ini} instance.
453      */
454     public static class Section implements Map<String, String> {
455         private final String name;
456         private final Map<String, String> props;
457 
458         private Section(String name) {
459             if (name == null) {
460                 throw new NullPointerException("name");
461             }
462             this.name = name;
463             this.props = new LinkedHashMap<String, String>();
464         }
465 
466         private Section(String name, String sectionContent) {
467             if (name == null) {
468                 throw new NullPointerException("name");
469             }
470             this.name = name;
471             Map<String,String> props;
472             if (StringUtils.hasText(sectionContent) ) {
473                 props = toMapProps(sectionContent);
474             } else {
475                 props = new LinkedHashMap<String,String>();
476             }
477             if ( props != null ) {
478                 this.props = props;
479             } else {
480                 this.props = new LinkedHashMap<String,String>();
481             }
482         }
483 
484         private Section(Section defaults) {
485             this(defaults.getName());
486             putAll(defaults.props);
487         }
488 
489         //Protected to access in a test case - NOT considered part of Shiro's public API
490 
491         protected static boolean isContinued(String line) {
492             if (!StringUtils.hasText(line)) {
493                 return false;
494             }
495             int length = line.length();
496             //find the number of backslashes at the end of the line.  If an even number, the
497             //backslashes are considered escaped.  If an odd number, the line is considered continued on the next line
498             int backslashCount = 0;
499             for (int i = length - 1; i > 0; i--) {
500                 if (line.charAt(i) == ESCAPE_TOKEN) {
501                     backslashCount++;
502                 } else {
503                     break;
504                 }
505             }
506             return backslashCount % 2 != 0;
507         }
508 
509         private static boolean isKeyValueSeparatorChar(char c) {
510             return Character.isWhitespace(c) || c == ':' || c == '=';
511         }
512 
513         private static boolean isCharEscaped(CharSequence s, int index) {
514             return index > 0 && s.charAt(index - 1) == ESCAPE_TOKEN;
515         }
516 
517         //Protected to access in a test case - NOT considered part of Shiro's public API
518         protected static String[] splitKeyValue(String keyValueLine) {
519             String line = StringUtils.clean(keyValueLine);
520             if (line == null) {
521                 return null;
522             }
523             StringBuilder keyBuffer = new StringBuilder();
524             StringBuilder valueBuffer = new StringBuilder();
525 
526             boolean buildingKey = true; //we'll build the value next:
527 
528             for (int i = 0; i < line.length(); i++) {
529                 char c = line.charAt(i);
530 
531                 if (buildingKey) {
532                     if (isKeyValueSeparatorChar(c) && !isCharEscaped(line, i)) {
533                         buildingKey = false;//now start building the value
534                     } else {
535                         keyBuffer.append(c);
536                     }
537                 } else {
538                     if (valueBuffer.length() == 0 && isKeyValueSeparatorChar(c) && !isCharEscaped(line, i)) {
539                         //swallow the separator chars before we start building the value
540                     } else {
541                         valueBuffer.append(c);
542                     }
543                 }
544             }
545 
546             String key = StringUtils.clean(keyBuffer.toString());
547             String value = StringUtils.clean(valueBuffer.toString());
548 
549             if (key == null || value == null) {
550                 String msg = "Line argument must contain a key and a value.  Only one string token was found.";
551                 throw new IllegalArgumentException(msg);
552             }
553 
554             log.trace("Discovered key/value pair: {} = {}", key, value);
555 
556             return new String[]{key, value};
557         }
558 
559         private static Map<String, String> toMapProps(String content) {
560             Map<String, String> props = new LinkedHashMap<String, String>();
561             String line;
562             StringBuilder lineBuffer = new StringBuilder();
563             Scanner scanner = new Scanner(content);
564             while (scanner.hasNextLine()) {
565                 line = StringUtils.clean(scanner.nextLine());
566                 if (isContinued(line)) {
567                     //strip off the last continuation backslash:
568                     line = line.substring(0, line.length() - 1);
569                     lineBuffer.append(line);
570                     continue;
571                 } else {
572                     lineBuffer.append(line);
573                 }
574                 line = lineBuffer.toString();
575                 lineBuffer = new StringBuilder();
576                 String[] kvPair = splitKeyValue(line);
577                 props.put(kvPair[0], kvPair[1]);
578             }
579 
580             return props;
581         }
582 
583         public String getName() {
584             return this.name;
585         }
586 
587         public void clear() {
588             this.props.clear();
589         }
590 
591         public boolean containsKey(Object key) {
592             return this.props.containsKey(key);
593         }
594 
595         public boolean containsValue(Object value) {
596             return this.props.containsValue(value);
597         }
598 
599         public Set<Entry<String, String>> entrySet() {
600             return this.props.entrySet();
601         }
602 
603         public String get(Object key) {
604             return this.props.get(key);
605         }
606 
607         public boolean isEmpty() {
608             return this.props.isEmpty();
609         }
610 
611         public Set<String> keySet() {
612             return this.props.keySet();
613         }
614 
615         public String put(String key, String value) {
616             return this.props.put(key, value);
617         }
618 
619         public void putAll(Map<? extends String, ? extends String> m) {
620             this.props.putAll(m);
621         }
622 
623         public String remove(Object key) {
624             return this.props.remove(key);
625         }
626 
627         public int size() {
628             return this.props.size();
629         }
630 
631         public Collection<String> values() {
632             return this.props.values();
633         }
634 
635         public String toString() {
636             String name = getName();
637             if (DEFAULT_SECTION_NAME.equals(name)) {
638                 return "<default>";
639             }
640             return name;
641         }
642 
643         @Override
644         public boolean equals(Object obj) {
645             if (obj instanceof Section) {
646                 Section other = (Section) obj;
647                 return getName().equals(other.getName()) && this.props.equals(other.props);
648             }
649             return false;
650         }
651 
652         @Override
653         public int hashCode() {
654             return this.name.hashCode() * 31 + this.props.hashCode();
655         }
656     }
657 
658 }