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.StringUtils; 023import org.slf4j.Logger; 024import org.slf4j.LoggerFactory; 025 026import java.io.IOException; 027import java.io.InputStream; 028import java.io.InputStreamReader; 029import java.io.Reader; 030import java.io.UnsupportedEncodingException; 031import java.util.Collection; 032import java.util.Collections; 033import java.util.LinkedHashMap; 034import java.util.Map; 035import java.util.Scanner; 036import java.util.Set; 037 038/** 039 * A class representing the <a href="http://en.wikipedia.org/wiki/INI_file">INI</a> text configuration format. 040 * <p/> 041 * An Ini instance is a map of {@link Ini.Section Section}s, keyed by section name. Each 042 * {@code Section} is itself a map of {@code String} name/value pairs. Name/value pairs are guaranteed to be unique 043 * within each {@code Section} only - not across the entire {@code Ini} instance. 044 * 045 * @since 1.0 046 */ 047public class Ini implements Map<String, Ini.Section> { 048 049 private static transient final Logger log = LoggerFactory.getLogger(Ini.class); 050 051 public static final String DEFAULT_SECTION_NAME = ""; //empty string means the first unnamed section 052 public static final String DEFAULT_CHARSET_NAME = "UTF-8"; 053 054 public static final String COMMENT_POUND = "#"; 055 public static final String COMMENT_SEMICOLON = ";"; 056 public static final String SECTION_PREFIX = "["; 057 public static final String SECTION_SUFFIX = "]"; 058 059 protected static final char ESCAPE_TOKEN = '\\'; 060 061 private final Map<String, Section> sections; 062 063 /** 064 * Creates a new empty {@code Ini} instance. 065 */ 066 public Ini() { 067 this.sections = new LinkedHashMap<String, Section>(); 068 } 069 070 /** 071 * Creates a new {@code Ini} instance with the specified defaults. 072 * 073 * @param defaults the default sections and/or key-value pairs to copy into the new instance. 074 */ 075 public Ini(Ini defaults) { 076 this(); 077 if (defaults == null) { 078 throw new NullPointerException("Defaults cannot be null."); 079 } 080 for (Section section : defaults.getSections()) { 081 Section copy = new Section(section); 082 this.sections.put(section.getName(), copy); 083 } 084 } 085 086 /** 087 * Returns {@code true} if no sections have been configured, or if there are sections, but the sections themselves 088 * are all empty, {@code false} otherwise. 089 * 090 * @return {@code true} if no sections have been configured, or if there are sections, but the sections themselves 091 * are all empty, {@code false} otherwise. 092 */ 093 public boolean isEmpty() { 094 Collection<Section> sections = this.sections.values(); 095 if (!sections.isEmpty()) { 096 for (Section section : sections) { 097 if (!section.isEmpty()) { 098 return false; 099 } 100 } 101 } 102 return true; 103 } 104 105 /** 106 * Returns the names of all sections managed by this {@code Ini} instance or an empty collection if there are 107 * no sections. 108 * 109 * @return the names of all sections managed by this {@code Ini} instance or an empty collection if there are 110 * no sections. 111 */ 112 public Set<String> getSectionNames() { 113 return Collections.unmodifiableSet(sections.keySet()); 114 } 115 116 /** 117 * Returns the sections managed by this {@code Ini} instance or an empty collection if there are 118 * no sections. 119 * 120 * @return the sections managed by this {@code Ini} instance or an empty collection if there are 121 * no sections. 122 */ 123 public Collection<Section> getSections() { 124 return Collections.unmodifiableCollection(sections.values()); 125 } 126 127 /** 128 * Returns the {@link Section} with the given name or {@code null} if no section with that name exists. 129 * 130 * @param sectionName the name of the section to retrieve. 131 * @return the {@link Section} with the given name or {@code null} if no section with that name exists. 132 */ 133 public Section getSection(String sectionName) { 134 String name = cleanName(sectionName); 135 return sections.get(name); 136 } 137 138 /** 139 * Ensures a section with the specified name exists, adding a new one if it does not yet exist. 140 * 141 * @param sectionName the name of the section to ensure existence 142 * @return the section created if it did not yet exist, or the existing Section that already existed. 143 */ 144 public Section addSection(String sectionName) { 145 String name = cleanName(sectionName); 146 Section section = getSection(name); 147 if (section == null) { 148 section = new Section(name); 149 this.sections.put(name, section); 150 } 151 return section; 152 } 153 154 /** 155 * Removes the section with the specified name and returns it, or {@code null} if the section did not exist. 156 * 157 * @param sectionName the name of the section to remove. 158 * @return the section with the specified name or {@code null} if the section did not exist. 159 */ 160 public Section removeSection(String sectionName) { 161 String name = cleanName(sectionName); 162 return this.sections.remove(name); 163 } 164 165 private static String cleanName(String sectionName) { 166 String name = StringUtils.clean(sectionName); 167 if (name == null) { 168 log.trace("Specified name was null or empty. Defaulting to the default section (name = \"\")"); 169 name = DEFAULT_SECTION_NAME; 170 } 171 return name; 172 } 173 174 /** 175 * Sets a name/value pair for the section with the given {@code sectionName}. If the section does not yet exist, 176 * it will be created. If the {@code sectionName} is null or empty, the name/value pair will be placed in the 177 * default (unnamed, empty string) section. 178 * 179 * @param sectionName the name of the section to add the name/value pair 180 * @param propertyName the name of the property to add 181 * @param propertyValue the property value 182 */ 183 public void setSectionProperty(String sectionName, String propertyName, String propertyValue) { 184 String name = cleanName(sectionName); 185 Section section = getSection(name); 186 if (section == null) { 187 section = addSection(name); 188 } 189 section.put(propertyName, propertyValue); 190 } 191 192 /** 193 * Returns the value of the specified section property, or {@code null} if the section or property do not exist. 194 * 195 * @param sectionName the name of the section to retrieve to acquire the property value 196 * @param propertyName the name of the section property for which to return the value 197 * @return the value of the specified section property, or {@code null} if the section or property do not exist. 198 */ 199 public String getSectionProperty(String sectionName, String propertyName) { 200 Section section = getSection(sectionName); 201 return section != null ? section.get(propertyName) : null; 202 } 203 204 /** 205 * Returns the value of the specified section property, or the {@code defaultValue} if the section or 206 * property do not exist. 207 * 208 * @param sectionName the name of the section to add the name/value pair 209 * @param propertyName the name of the property to add 210 * @param defaultValue the default value to return if the section or property do not exist. 211 * @return the value of the specified section property, or the {@code defaultValue} if the section or 212 * property do not exist. 213 */ 214 public String getSectionProperty(String sectionName, String propertyName, String defaultValue) { 215 String value = getSectionProperty(sectionName, propertyName); 216 return value != null ? value : defaultValue; 217 } 218 219 /** 220 * Creates a new {@code Ini} instance loaded with the INI-formatted data in the resource at the given path. The 221 * resource path may be any value interpretable by the 222 * {@link ResourceUtils#getInputStreamForPath(String) ResourceUtils.getInputStreamForPath} method. 223 * 224 * @param resourcePath the resource location of the INI data to load when creating the {@code Ini} instance. 225 * @return a new {@code Ini} instance loaded with the INI-formatted data in the resource at the given path. 226 * @throws ConfigurationException if the path cannot be loaded into an {@code Ini} instance. 227 */ 228 public static Ini fromResourcePath(String resourcePath) throws ConfigurationException { 229 if (!StringUtils.hasLength(resourcePath)) { 230 throw new IllegalArgumentException("Resource Path argument cannot be null or empty."); 231 } 232 Ini ini = new Ini(); 233 ini.loadFromPath(resourcePath); 234 return ini; 235 } 236 237 /** 238 * Loads data from the specified resource path into this current {@code Ini} instance. The 239 * resource path may be any value interpretable by the 240 * {@link ResourceUtils#getInputStreamForPath(String) ResourceUtils.getInputStreamForPath} method. 241 * 242 * @param resourcePath the resource location of the INI data to load into this instance. 243 * @throws ConfigurationException if the path cannot be loaded 244 */ 245 public void loadFromPath(String resourcePath) throws ConfigurationException { 246 InputStream is; 247 try { 248 is = ResourceUtils.getInputStreamForPath(resourcePath); 249 } catch (IOException e) { 250 throw new ConfigurationException(e); 251 } 252 load(is); 253 } 254 255 /** 256 * Loads the specified raw INI-formatted text into this instance. 257 * 258 * @param iniConfig the raw INI-formatted text to load into this instance. 259 * @throws ConfigurationException if the text cannot be loaded 260 */ 261 public void load(String iniConfig) throws ConfigurationException { 262 load(new Scanner(iniConfig)); 263 } 264 265 /** 266 * Loads the INI-formatted text backed by the given InputStream into this instance. This implementation will 267 * close the input stream after it has finished loading. It is expected that the stream's contents are 268 * UTF-8 encoded. 269 * 270 * @param is the {@code InputStream} from which to read the INI-formatted text 271 * @throws ConfigurationException if unable 272 */ 273 public void load(InputStream is) throws ConfigurationException { 274 if (is == null) { 275 throw new NullPointerException("InputStream argument cannot be null."); 276 } 277 InputStreamReader isr; 278 try { 279 isr = new InputStreamReader(is, DEFAULT_CHARSET_NAME); 280 } catch (UnsupportedEncodingException e) { 281 throw new ConfigurationException(e); 282 } 283 load(isr); 284 } 285 286 /** 287 * Loads the INI-formatted text backed by the given Reader into this instance. This implementation will close the 288 * reader after it has finished loading. 289 * 290 * @param reader the {@code Reader} from which to read the INI-formatted text 291 */ 292 public void load(Reader reader) { 293 Scanner scanner = new Scanner(reader); 294 try { 295 load(scanner); 296 } finally { 297 try { 298 scanner.close(); 299 } catch (Exception e) { 300 log.debug("Unable to cleanly close the InputStream scanner. Non-critical - ignoring.", e); 301 } 302 } 303 } 304 305 /** 306 * Merges the contents of <code>m</code>'s {@link Section} objects into self. 307 * This differs from {@link Ini#putAll(Map)}, in that each section is merged with the existing one. 308 * For example the following two ini blocks are merged and the result is the third<BR/> 309 * <p> 310 * Initial: 311 * <pre> 312 * <code>[section1] 313 * key1 = value1 314 * 315 * [section2] 316 * key2 = value2 317 * </code> </pre> 318 * 319 * To be merged: 320 * <pre> 321 * <code>[section1] 322 * foo = bar 323 * 324 * [section2] 325 * key2 = new value 326 * </code> </pre> 327 * 328 * Result: 329 * <pre> 330 * <code>[section1] 331 * key1 = value1 332 * foo = bar 333 * 334 * [section2] 335 * key2 = new value 336 * </code> </pre> 337 * 338 * </p> 339 * 340 * @param m map to be merged 341 * @since 1.4 342 */ 343 public void merge(Map<String, Section> m) { 344 345 if (m != null) { 346 for (Entry<String, Section> entry : m.entrySet()) { 347 Section section = this.getSection(entry.getKey()); 348 if (section == null) { 349 section = addSection(entry.getKey()); 350 } 351 section.putAll(entry.getValue()); 352 } 353 } 354 } 355 356 private void addSection(String name, StringBuilder content) { 357 if (content.length() > 0) { 358 String contentString = content.toString(); 359 String cleaned = StringUtils.clean(contentString); 360 if (cleaned != null) { 361 Section section = new Section(name, contentString); 362 if (!section.isEmpty()) { 363 sections.put(name, section); 364 } 365 } 366 } 367 } 368 369 /** 370 * Loads the INI-formatted text backed by the given Scanner. This implementation will close the 371 * scanner after it has finished loading. 372 * 373 * @param scanner the {@code Scanner} from which to read the INI-formatted text 374 */ 375 public void load(Scanner scanner) { 376 377 String sectionName = DEFAULT_SECTION_NAME; 378 StringBuilder sectionContent = new StringBuilder(); 379 380 while (scanner.hasNextLine()) { 381 382 String rawLine = scanner.nextLine(); 383 String line = StringUtils.clean(rawLine); 384 385 if (line == null || line.startsWith(COMMENT_POUND) || line.startsWith(COMMENT_SEMICOLON)) { 386 //skip empty lines and comments: 387 continue; 388 } 389 390 String newSectionName = getSectionName(line); 391 if (newSectionName != null) { 392 //found a new section - convert the currently buffered one into a Section object 393 addSection(sectionName, sectionContent); 394 395 //reset the buffer for the new section: 396 sectionContent = new StringBuilder(); 397 398 sectionName = newSectionName; 399 400 if (log.isDebugEnabled()) { 401 log.debug("Parsing " + SECTION_PREFIX + sectionName + SECTION_SUFFIX); 402 } 403 } else { 404 //normal line - add it to the existing content buffer: 405 sectionContent.append(rawLine).append("\n"); 406 } 407 } 408 409 //finish any remaining buffered content: 410 addSection(sectionName, sectionContent); 411 } 412 413 protected static boolean isSectionHeader(String line) { 414 String s = StringUtils.clean(line); 415 return s != null && s.startsWith(SECTION_PREFIX) && s.endsWith(SECTION_SUFFIX); 416 } 417 418 protected static String getSectionName(String line) { 419 String s = StringUtils.clean(line); 420 if (isSectionHeader(s)) { 421 return cleanName(s.substring(1, s.length() - 1)); 422 } 423 return null; 424 } 425 426 public boolean equals(Object obj) { 427 if (obj instanceof Ini) { 428 Ini ini = (Ini) obj; 429 return this.sections.equals(ini.sections); 430 } 431 return false; 432 } 433 434 @Override 435 public int hashCode() { 436 return this.sections.hashCode(); 437 } 438 439 public String toString() { 440 if (this.sections == null || this.sections.isEmpty()) { 441 return "<empty INI>"; 442 } else { 443 StringBuilder sb = new StringBuilder("sections="); 444 int i = 0; 445 for (Ini.Section section : this.sections.values()) { 446 if (i > 0) { 447 sb.append(","); 448 } 449 sb.append(section.toString()); 450 i++; 451 } 452 return sb.toString(); 453 } 454 } 455 456 public int size() { 457 return this.sections.size(); 458 } 459 460 public boolean containsKey(Object key) { 461 return this.sections.containsKey(key); 462 } 463 464 public boolean containsValue(Object value) { 465 return this.sections.containsValue(value); 466 } 467 468 public Section get(Object key) { 469 return this.sections.get(key); 470 } 471 472 public Section put(String key, Section value) { 473 return this.sections.put(key, value); 474 } 475 476 public Section remove(Object key) { 477 return this.sections.remove(key); 478 } 479 480 public void putAll(Map<? extends String, ? extends Section> m) { 481 this.sections.putAll(m); 482 } 483 484 public void clear() { 485 this.sections.clear(); 486 } 487 488 public Set<String> keySet() { 489 return Collections.unmodifiableSet(this.sections.keySet()); 490 } 491 492 public Collection<Section> values() { 493 return Collections.unmodifiableCollection(this.sections.values()); 494 } 495 496 public Set<Entry<String, Section>> entrySet() { 497 return Collections.unmodifiableSet(this.sections.entrySet()); 498 } 499 500 /** 501 * An {@code Ini.Section} is String-key-to-String-value Map, identifiable by a 502 * {@link #getName() name} unique within an {@link Ini} instance. 503 */ 504 public static class Section implements Map<String, String> { 505 private final String name; 506 private final Map<String, String> props; 507 508 private Section(String name) { 509 if (name == null) { 510 throw new NullPointerException("name"); 511 } 512 this.name = name; 513 this.props = new LinkedHashMap<String, String>(); 514 } 515 516 private Section(String name, String sectionContent) { 517 if (name == null) { 518 throw new NullPointerException("name"); 519 } 520 this.name = name; 521 Map<String,String> props; 522 if (StringUtils.hasText(sectionContent) ) { 523 props = toMapProps(sectionContent); 524 } else { 525 props = new LinkedHashMap<String,String>(); 526 } 527 if ( props != null ) { 528 this.props = props; 529 } else { 530 this.props = new LinkedHashMap<String,String>(); 531 } 532 } 533 534 private Section(Section defaults) { 535 this(defaults.getName()); 536 putAll(defaults.props); 537 } 538 539 //Protected to access in a test case - NOT considered part of Shiro's public API 540 541 protected static boolean isContinued(String line) { 542 if (!StringUtils.hasText(line)) { 543 return false; 544 } 545 int length = line.length(); 546 //find the number of backslashes at the end of the line. If an even number, the 547 //backslashes are considered escaped. If an odd number, the line is considered continued on the next line 548 int backslashCount = 0; 549 for (int i = length - 1; i > 0; i--) { 550 if (line.charAt(i) == ESCAPE_TOKEN) { 551 backslashCount++; 552 } else { 553 break; 554 } 555 } 556 return backslashCount % 2 != 0; 557 } 558 559 private static boolean isKeyValueSeparatorChar(char c) { 560 return Character.isWhitespace(c) || c == ':' || c == '='; 561 } 562 563 private static boolean isCharEscaped(CharSequence s, int index) { 564 return index > 0 && s.charAt(index - 1) == ESCAPE_TOKEN; 565 } 566 567 //Protected to access in a test case - NOT considered part of Shiro's public API 568 protected static String[] splitKeyValue(String keyValueLine) { 569 String line = StringUtils.clean(keyValueLine); 570 if (line == null) { 571 return null; 572 } 573 StringBuilder keyBuffer = new StringBuilder(); 574 StringBuilder valueBuffer = new StringBuilder(); 575 576 boolean buildingKey = true; //we'll build the value next: 577 578 for (int i = 0; i < line.length(); i++) { 579 char c = line.charAt(i); 580 581 if (buildingKey) { 582 if (isKeyValueSeparatorChar(c) && !isCharEscaped(line, i)) { 583 buildingKey = false;//now start building the value 584 } else { 585 keyBuffer.append(c); 586 } 587 } else { 588 if (valueBuffer.length() == 0 && isKeyValueSeparatorChar(c) && !isCharEscaped(line, i)) { 589 //swallow the separator chars before we start building the value 590 } else { 591 valueBuffer.append(c); 592 } 593 } 594 } 595 596 String key = StringUtils.clean(keyBuffer.toString()); 597 String value = StringUtils.clean(valueBuffer.toString()); 598 599 if (key == null || value == null) { 600 String msg = "Line argument must contain a key and a value. Only one string token was found."; 601 throw new IllegalArgumentException(msg); 602 } 603 604 log.trace("Discovered key/value pair: {} = {}", key, value); 605 606 return new String[]{key, value}; 607 } 608 609 private static Map<String, String> toMapProps(String content) { 610 Map<String, String> props = new LinkedHashMap<String, String>(); 611 String line; 612 StringBuilder lineBuffer = new StringBuilder(); 613 Scanner scanner = new Scanner(content); 614 while (scanner.hasNextLine()) { 615 line = StringUtils.clean(scanner.nextLine()); 616 if (isContinued(line)) { 617 //strip off the last continuation backslash: 618 line = line.substring(0, line.length() - 1); 619 lineBuffer.append(line); 620 continue; 621 } else { 622 lineBuffer.append(line); 623 } 624 line = lineBuffer.toString(); 625 lineBuffer = new StringBuilder(); 626 String[] kvPair = splitKeyValue(line); 627 props.put(kvPair[0], kvPair[1]); 628 } 629 630 return props; 631 } 632 633 public String getName() { 634 return this.name; 635 } 636 637 public void clear() { 638 this.props.clear(); 639 } 640 641 public boolean containsKey(Object key) { 642 return this.props.containsKey(key); 643 } 644 645 public boolean containsValue(Object value) { 646 return this.props.containsValue(value); 647 } 648 649 public Set<Entry<String, String>> entrySet() { 650 return this.props.entrySet(); 651 } 652 653 public String get(Object key) { 654 return this.props.get(key); 655 } 656 657 public boolean isEmpty() { 658 return this.props.isEmpty(); 659 } 660 661 public Set<String> keySet() { 662 return this.props.keySet(); 663 } 664 665 public String put(String key, String value) { 666 return this.props.put(key, value); 667 } 668 669 public void putAll(Map<? extends String, ? extends String> m) { 670 this.props.putAll(m); 671 } 672 673 public String remove(Object key) { 674 return this.props.remove(key); 675 } 676 677 public int size() { 678 return this.props.size(); 679 } 680 681 public Collection<String> values() { 682 return this.props.values(); 683 } 684 685 public String toString() { 686 String name = getName(); 687 if (DEFAULT_SECTION_NAME.equals(name)) { 688 return "<default>"; 689 } 690 return name; 691 } 692 693 @Override 694 public boolean equals(Object obj) { 695 if (obj instanceof Section) { 696 Section other = (Section) obj; 697 return getName().equals(other.getName()) && this.props.equals(other.props); 698 } 699 return false; 700 } 701 702 @Override 703 public int hashCode() { 704 return this.name.hashCode() * 31 + this.props.hashCode(); 705 } 706 } 707 708}