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