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