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 }