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     */
019    package org.apache.shiro.config;
020    
021    import org.apache.shiro.io.ResourceUtils;
022    import org.apache.shiro.util.CollectionUtils;
023    import org.apache.shiro.util.StringUtils;
024    import org.slf4j.Logger;
025    import org.slf4j.LoggerFactory;
026    
027    import java.io.*;
028    import 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     */
039    public 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    }