001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.config;
020
021import org.apache.shiro.io.ResourceUtils;
022import org.apache.shiro.util.StringUtils;
023import org.slf4j.Logger;
024import org.slf4j.LoggerFactory;
025
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.InputStreamReader;
029import java.io.Reader;
030import java.io.UnsupportedEncodingException;
031import java.util.Collection;
032import java.util.Collections;
033import java.util.LinkedHashMap;
034import java.util.Map;
035import java.util.Scanner;
036import java.util.Set;
037
038/**
039 * A class representing the <a href="http://en.wikipedia.org/wiki/INI_file">INI</a> text configuration format.
040 * <p/>
041 * An Ini instance is a map of {@link Ini.Section Section}s, keyed by section name.  Each
042 * {@code Section} is itself a map of {@code String} name/value pairs.  Name/value pairs are guaranteed to be unique
043 * within each {@code Section} only - not across the entire {@code Ini} instance.
044 *
045 * @since 1.0
046 */
047public class Ini implements Map<String, Ini.Section> {
048
049    private static transient final Logger log = LoggerFactory.getLogger(Ini.class);
050
051    public static final String DEFAULT_SECTION_NAME = ""; //empty string means the first unnamed section
052    public static final String DEFAULT_CHARSET_NAME = "UTF-8";
053
054    public static final String COMMENT_POUND = "#";
055    public static final String COMMENT_SEMICOLON = ";";
056    public static final String SECTION_PREFIX = "[";
057    public static final String SECTION_SUFFIX = "]";
058
059    protected static final char ESCAPE_TOKEN = '\\';
060
061    private final Map<String, Section> sections;
062
063    /**
064     * Creates a new empty {@code Ini} instance.
065     */
066    public Ini() {
067        this.sections = new LinkedHashMap<String, Section>();
068    }
069
070    /**
071     * Creates a new {@code Ini} instance with the specified defaults.
072     *
073     * @param defaults the default sections and/or key-value pairs to copy into the new instance.
074     */
075    public Ini(Ini defaults) {
076        this();
077        if (defaults == null) {
078            throw new NullPointerException("Defaults cannot be null.");
079        }
080        for (Section section : defaults.getSections()) {
081            Section copy = new Section(section);
082            this.sections.put(section.getName(), copy);
083        }
084    }
085
086    /**
087     * Returns {@code true} if no sections have been configured, or if there are sections, but the sections themselves
088     * are all empty, {@code false} otherwise.
089     *
090     * @return {@code true} if no sections have been configured, or if there are sections, but the sections themselves
091     *         are all empty, {@code false} otherwise.
092     */
093    public boolean isEmpty() {
094        Collection<Section> sections = this.sections.values();
095        if (!sections.isEmpty()) {
096            for (Section section : sections) {
097                if (!section.isEmpty()) {
098                    return false;
099                }
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        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            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}