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.CollectionUtils;
023import org.apache.shiro.util.StringUtils;
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027import java.io.IOException;
028import java.io.InputStream;
029import java.io.InputStreamReader;
030import java.io.Reader;
031import java.io.UnsupportedEncodingException;
032import java.util.Collection;
033import java.util.Collections;
034import java.util.LinkedHashMap;
035import java.util.Map;
036import java.util.Scanner;
037import java.util.Set;
038
039/**
040 * A class representing the <a href="http://en.wikipedia.org/wiki/INI_file">INI</a> text configuration format.
041 * <p/>
042 * An Ini instance is a map of {@link Ini.Section Section}s, keyed by section name.  Each
043 * {@code Section} is itself a map of {@code String} name/value pairs.  Name/value pairs are guaranteed to be unique
044 * within each {@code Section} only - not across the entire {@code Ini} instance.
045 *
046 * @since 1.0
047 */
048public class Ini implements Map<String, Ini.Section> {
049
050    private static transient final Logger log = LoggerFactory.getLogger(Ini.class);
051
052    public static final String DEFAULT_SECTION_NAME = ""; //empty string means the first unnamed section
053    public static final String DEFAULT_CHARSET_NAME = "UTF-8";
054
055    public static final String COMMENT_POUND = "#";
056    public static final String COMMENT_SEMICOLON = ";";
057    public static final String SECTION_PREFIX = "[";
058    public static final String SECTION_SUFFIX = "]";
059
060    protected static final char ESCAPE_TOKEN = '\\';
061
062    private final Map<String, Section> sections;
063
064    /**
065     * Creates a new empty {@code Ini} instance.
066     */
067    public Ini() {
068        this.sections = new LinkedHashMap<String, Section>();
069    }
070
071    /**
072     * Creates a new {@code Ini} instance with the specified defaults.
073     *
074     * @param defaults the default sections and/or key-value pairs to copy into the new instance.
075     */
076    public Ini(Ini defaults) {
077        this();
078        if (defaults == null) {
079            throw new NullPointerException("Defaults cannot be null.");
080        }
081        for (Section section : defaults.getSections()) {
082            Section copy = new Section(section);
083            this.sections.put(section.getName(), copy);
084        }
085    }
086
087    /**
088     * Returns {@code true} if no sections have been configured, or if there are sections, but the sections themselves
089     * are all empty, {@code false} otherwise.
090     *
091     * @return {@code true} if no sections have been configured, or if there are sections, but the sections themselves
092     *         are all empty, {@code false} otherwise.
093     */
094    public boolean isEmpty() {
095        Collection<Section> sections = this.sections.values();
096        if (!sections.isEmpty()) {
097            for (Section section : sections) {
098                if (!section.isEmpty()) {
099                    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}