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     * @author The Apache Shiro Project (shiro-dev@incubator.apache.org)
038     * @since 1.0
039     */
040    public class Ini implements Map<String, Ini.Section> {
041    
042        private static transient final Logger log = LoggerFactory.getLogger(Ini.class);
043    
044        public static final String DEFAULT_SECTION_NAME = ""; //empty string means the first unnamed section
045        public static final String DEFAULT_CHARSET_NAME = "ISO-8859-1";
046    
047        public static final String COMMENT_POUND = "#";
048        public static final String COMMENT_SEMICOLON = ";";
049        public static final String SECTION_PREFIX = "[";
050        public static final String SECTION_SUFFIX = "]";
051    
052        private final Map<String, Section> sections;
053    
054        /**
055         * Creates a new empty {@code Ini} instance.
056         */
057        public Ini() {
058            this.sections = new LinkedHashMap<String, Section>();
059        }
060    
061        /**
062         * Creates a new {@code Ini} instance with the specified defaults.
063         *
064         * @param defaults the default sections and/or key-value pairs to copy into the new instance.
065         */
066        public Ini(Ini defaults) {
067            this();
068            if (defaults == null) {
069                throw new NullPointerException("Defaults cannot be null.");
070            }
071            for (Section section : defaults.getSections()) {
072                Section copy = new Section(section);
073                this.sections.put(section.getName(), copy);
074            }
075        }
076    
077        /**
078         * Returns {@code true} if no sections have been configured, or if there are sections, but the sections themselves
079         * are all empty, {@code false} otherwise.
080         *
081         * @return {@code true} if no sections have been configured, or if there are sections, but the sections themselves
082         *         are all empty, {@code false} otherwise.
083         */
084        public boolean isEmpty() {
085            Collection<Section> sections = this.sections.values();
086            if (!sections.isEmpty()) {
087                for (Section section : sections) {
088                    if (!section.isEmpty()) {
089                        return false;
090                    }
091                }
092            }
093            return true;
094        }
095    
096        /**
097         * Returns the names of all sections managed by this {@code Ini} instance or an empty collection if there are
098         * no sections.
099         *
100         * @return the names of all sections managed by this {@code Ini} instance or an empty collection if there are
101         *         no sections.
102         */
103        public Set<String> getSectionNames() {
104            return Collections.unmodifiableSet(sections.keySet());
105        }
106    
107        /**
108         * Returns the sections managed by this {@code Ini} instance or an empty collection if there are
109         * no sections.
110         *
111         * @return the sections managed by this {@code Ini} instance or an empty collection if there are
112         *         no sections.
113         */
114        public Collection<Section> getSections() {
115            return Collections.unmodifiableCollection(sections.values());
116        }
117    
118        /**
119         * Returns the {@link Section} with the given name or {@code null} if no section with that name exists.
120         *
121         * @param sectionName the name of the section to retrieve.
122         * @return the {@link Section} with the given name or {@code null} if no section with that name exists.
123         */
124        public Section getSection(String sectionName) {
125            String name = cleanName(sectionName);
126            return sections.get(name);
127        }
128    
129        /**
130         * Ensures a section with the specified name exists, adding a new one if it does not yet exist.
131         *
132         * @param sectionName the name of the section to ensure existence
133         * @return the section created if it did not yet exist, or the existing Section that already existed.
134         */
135        public Section addSection(String sectionName) {
136            String name = cleanName(sectionName);
137            Section section = getSection(name);
138            if (section == null) {
139                section = new Section(name);
140                this.sections.put(name, section);
141            }
142            return section;
143        }
144    
145        /**
146         * Removes the section with the specified name and returns it, or {@code null} if the section did not exist.
147         *
148         * @param sectionName the name of the section to remove.
149         * @return the section with the specified name or {@code null} if the section did not exist.
150         */
151        public Section removeSection(String sectionName) {
152            String name = cleanName(sectionName);
153            return this.sections.remove(name);
154        }
155    
156        private static String cleanName(String sectionName) {
157            String name = StringUtils.clean(sectionName);
158            if (name == null) {
159                log.trace("Specified name was null or empty.  Defaulting to the default section (name = \"\")");
160                name = DEFAULT_SECTION_NAME;
161            }
162            return name;
163        }
164    
165        /**
166         * Sets a name/value pair for the section with the given {@code sectionName}.  If the section does not yet exist,
167         * it will be created.  If the {@code sectionName} is null or empty, the name/value pair will be placed in the
168         * default (unnamed, empty string) section.
169         *
170         * @param sectionName   the name of the section to add the name/value pair
171         * @param propertyName  the name of the property to add
172         * @param propertyValue the property value
173         */
174        public void setSectionProperty(String sectionName, String propertyName, String propertyValue) {
175            String name = cleanName(sectionName);
176            Section section = getSection(name);
177            if (section == null) {
178                section = addSection(name);
179            }
180            section.put(propertyName, propertyValue);
181        }
182    
183        /**
184         * Returns the value of the specified section property, or {@code null} if the section or property do not exist.
185         *
186         * @param sectionName  the name of the section to retrieve to acquire the property value
187         * @param propertyName the name of the section property for which to return the value
188         * @return the value of the specified section property, or {@code null} if the section or property do not exist.
189         */
190        public String getSectionProperty(String sectionName, String propertyName) {
191            Section section = getSection(sectionName);
192            return section != null ? section.get(propertyName) : null;
193        }
194    
195        /**
196         * Returns the value of the specified section property, or the {@code defaultValue} if the section or
197         * property do not exist.
198         *
199         * @param sectionName  the name of the section to add the name/value pair
200         * @param propertyName the name of the property to add
201         * @param defaultValue the default value to return if the section or property do not exist.
202         * @return the value of the specified section property, or the {@code defaultValue} if the section or
203         *         property do not exist.
204         */
205        public String getSectionProperty(String sectionName, String propertyName, String defaultValue) {
206            String value = getSectionProperty(sectionName, propertyName);
207            return value != null ? value : defaultValue;
208        }
209    
210        /**
211         * Creates a new {@code Ini} instance loaded with the INI-formatted data in the resource at the given path.  The
212         * resource path may be any value interpretable by the
213         * {@link ResourceUtils#getInputStreamForPath(String) ResourceUtils.getInputStreamForPath} method.
214         *
215         * @param resourcePath the resource location of the INI data to load when creating the {@code Ini} instance.
216         * @return a new {@code Ini} instance loaded with the INI-formatted data in the resource at the given path.
217         * @throws ConfigurationException if the path cannot be loaded into an {@code Ini} instance.
218         */
219        public static Ini fromResourcePath(String resourcePath) throws ConfigurationException {
220            if (!StringUtils.hasLength(resourcePath)) {
221                throw new IllegalArgumentException("Resource Path argument cannot be null or empty.");
222            }
223            Ini ini = new Ini();
224            ini.loadFromPath(resourcePath);
225            return ini;
226        }
227    
228        /**
229         * Loads data from the specified resource path into this current {@code Ini} instance.  The
230         * resource path may be any value interpretable by the
231         * {@link ResourceUtils#getInputStreamForPath(String) ResourceUtils.getInputStreamForPath} method.
232         *
233         * @param resourcePath the resource location of the INI data to load into this instance.
234         * @throws ConfigurationException if the path cannot be loaded
235         */
236        public void loadFromPath(String resourcePath) throws ConfigurationException {
237            InputStream is;
238            try {
239                is = ResourceUtils.getInputStreamForPath(resourcePath);
240            } catch (IOException e) {
241                throw new ConfigurationException(e);
242            }
243            load(is);
244        }
245    
246        /**
247         * Loads the specified raw INI-formatted text into this instance.
248         *
249         * @param iniConfig the raw INI-formatted text to load into this instance.
250         * @throws ConfigurationException if the text cannot be loaded
251         */
252        public void load(String iniConfig) throws ConfigurationException {
253            load(new Scanner(iniConfig));
254        }
255    
256        /**
257         * Loads the INI-formatted text backed by the given InputStream into this instance.  This implementation will
258         * close the input stream after it has finished loading.
259         *
260         * @param is the {@code InputStream} from which to read the INI-formatted text
261         * @throws ConfigurationException if unable
262         */
263        public void load(InputStream is) throws ConfigurationException {
264            if (is == null) {
265                throw new NullPointerException("InputStream argument cannot be null.");
266            }
267            InputStreamReader isr;
268            try {
269                isr = new InputStreamReader(is, DEFAULT_CHARSET_NAME);
270            } catch (UnsupportedEncodingException e) {
271                throw new ConfigurationException(e);
272            }
273            load(isr);
274        }
275    
276        /**
277         * Loads the INI-formatted text backed by the given Reader into this instance.  This implementation will close the
278         * reader after it has finished loading.
279         *
280         * @param reader the {@code Reader} from which to read the INI-formatted text
281         */
282        public void load(Reader reader) {
283            Scanner scanner = new Scanner(reader);
284            try {
285                load(scanner);
286            } finally {
287                try {
288                    scanner.close();
289                } catch (Exception e) {
290                    log.debug("Unable to cleanly close the InputStream scanner.  Non-critical - ignoring.", e);
291                }
292            }
293        }
294    
295        private static InputStream toInputStream(String content) {
296            byte[] bytes;
297            try {
298                bytes = content.getBytes(DEFAULT_CHARSET_NAME);
299            } catch (UnsupportedEncodingException e) {
300                throw new ConfigurationException(e);
301            }
302            return new ByteArrayInputStream(bytes);
303        }
304    
305        private static Properties toProps(String content) {
306            InputStream is = toInputStream(content);
307            Properties props = new Properties();
308            try {
309                props.load(is);
310            } catch (IOException e) {
311                throw new ConfigurationException(e);
312            }
313            return props;
314        }
315    
316        private void addSection(String name, StringBuffer content) {
317            if (content.length() > 0) {
318                String contentString = content.toString();
319                String cleaned = StringUtils.clean(contentString);
320                if (cleaned != null) {
321                    Properties props = toProps(contentString);
322                    if (!props.isEmpty()) {
323                        sections.put(name, new Section(name, props));
324                    }
325                }
326            }
327        }
328    
329        /**
330         * Loads the INI-formatted text backed by the given Scanner.  This implementation will close the
331         * scanner after it has finished loading.
332         *
333         * @param scanner the {@code Scanner} from which to read the INI-formatted text
334         */
335        public void load(Scanner scanner) {
336    
337            String sectionName = DEFAULT_SECTION_NAME;
338            StringBuffer sectionContent = new StringBuffer();
339    
340            while (scanner.hasNextLine()) {
341    
342                String rawLine = scanner.nextLine();
343                String line = StringUtils.clean(rawLine);
344    
345                if (line == null || line.startsWith(COMMENT_POUND) || line.startsWith(COMMENT_SEMICOLON)) {
346                    //skip empty lines and comments:
347                    continue;
348                }
349    
350                String newSectionName = getSectionName(line);
351                if (newSectionName != null) {
352                    //found a new section - convert the currently buffered one into a Section object
353                    addSection(sectionName, sectionContent);
354    
355                    //reset the buffer for the new section:
356                    sectionContent = new StringBuffer();
357    
358                    sectionName = newSectionName;
359    
360                    if (log.isDebugEnabled()) {
361                        log.debug("Parsing " + SECTION_PREFIX + sectionName + SECTION_SUFFIX);
362                    }
363                } else {
364                    //normal line - add it to the existing content buffer:
365                    sectionContent.append(rawLine).append("\n");
366                }
367            }
368    
369            //finish any remaining buffered content:
370            addSection(sectionName, sectionContent);
371        }
372    
373        protected static boolean isSectionHeader(String line) {
374            String s = StringUtils.clean(line);
375            return s != null && s.startsWith(SECTION_PREFIX) && s.endsWith(SECTION_SUFFIX);
376        }
377    
378        protected static String getSectionName(String line) {
379            String s = StringUtils.clean(line);
380            if (isSectionHeader(s)) {
381                return cleanName(s.substring(1, s.length() - 1));
382            }
383            return null;
384        }
385    
386        public boolean equals(Object obj) {
387            if (obj instanceof Ini) {
388                Ini ini = (Ini) obj;
389                return this.sections.equals(ini.sections);
390            }
391            return false;
392        }
393    
394        @Override
395        public int hashCode() {
396            return this.sections.hashCode();
397        }
398    
399        public String toString() {
400            if (CollectionUtils.isEmpty(this.sections)) {
401                return "<empty INI>";
402            } else {
403                StringBuilder sb = new StringBuilder("sections=");
404                int i = 0;
405                for (Ini.Section section : this.sections.values()) {
406                    if (i > 0) {
407                        sb.append(",");
408                    }
409                    sb.append(section.toString());
410                    i++;
411                }
412                return sb.toString();
413            }
414        }
415    
416        public int size() {
417            return this.sections.size();
418        }
419    
420        public boolean containsKey(Object key) {
421            return this.sections.containsKey(key);
422        }
423    
424        public boolean containsValue(Object value) {
425            return this.sections.containsValue(value);
426        }
427    
428        public Section get(Object key) {
429            return this.sections.get(key);
430        }
431    
432        public Section put(String key, Section value) {
433            return this.sections.put(key, value);
434        }
435    
436        public Section remove(Object key) {
437            return this.sections.remove(key);
438        }
439    
440        public void putAll(Map<? extends String, ? extends Section> m) {
441            this.sections.putAll(m);
442        }
443    
444        public void clear() {
445            this.sections.clear();
446        }
447    
448        public Set<String> keySet() {
449            return Collections.unmodifiableSet(this.sections.keySet());
450        }
451    
452        public Collection<Section> values() {
453            return Collections.unmodifiableCollection(this.sections.values());
454        }
455    
456        public Set<Entry<String, Section>> entrySet() {
457            return Collections.unmodifiableSet(this.sections.entrySet());
458        }
459    
460        /**
461         * An {@code Ini.Section} is String-key-to-String-value Map, identifiable by a
462         * {@link #getName() name} unique within an {@link Ini} instance.
463         */
464        public class Section implements Map<String, String> {
465            private final String name;
466            private final Map<String, String> props;
467    
468            private Section(String name) {
469                if (name == null) {
470                    throw new NullPointerException("name");
471                }
472                this.name = name;
473                this.props = new LinkedHashMap<String, String>();
474            }
475    
476            private Section(String name, Properties props) {
477                this(name);
478                Enumeration propNames = props.propertyNames();
479                while (propNames != null && propNames.hasMoreElements()) {
480                    String key = propNames.nextElement().toString();
481                    String value = props.getProperty(key);
482                    if (value != null) {
483                        this.props.put(key, value.trim());
484                    }
485                }
486            }
487    
488            private Section(Section defaults) {
489                this(defaults.getName());
490                putAll(defaults.props);
491            }
492    
493            public String getName() {
494                return this.name;
495            }
496    
497            public void clear() {
498                this.props.clear();
499            }
500    
501            public boolean containsKey(Object key) {
502                return this.props.containsKey(key);
503            }
504    
505            public boolean containsValue(Object value) {
506                return this.props.containsValue(value);
507            }
508    
509            public Set<Entry<String, String>> entrySet() {
510                return this.props.entrySet();
511            }
512    
513            public String get(Object key) {
514                return this.props.get(key);
515            }
516    
517            public boolean isEmpty() {
518                return this.props.isEmpty();
519            }
520    
521            public Set<String> keySet() {
522                return this.props.keySet();
523            }
524    
525            public String put(String key, String value) {
526                return this.props.put(key, value);
527            }
528    
529            public void putAll(Map<? extends String, ? extends String> m) {
530                this.props.putAll(m);
531            }
532    
533            public String remove(Object key) {
534                return this.props.remove(key);
535            }
536    
537            public int size() {
538                return this.props.size();
539            }
540    
541            public Collection<String> values() {
542                return this.props.values();
543            }
544    
545            public String toString() {
546                String name = getName();
547                if (DEFAULT_SECTION_NAME.equals(name)) {
548                    return "<default>";
549                }
550                return name;
551            }
552    
553            @Override
554            public boolean equals(Object obj) {
555                if (obj instanceof Section) {
556                    Section other = (Section) obj;
557                    return getName().equals(other.getName()) && this.props.equals(other.props);
558                }
559                return false;
560            }
561    
562            @Override
563            public int hashCode() {
564                return this.name.hashCode() * 31 + this.props.hashCode();
565            }
566        }
567    
568    }