001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package org.apache.commons.configuration; 018 019 import java.io.IOException; 020 import java.io.Reader; 021 import java.io.Writer; 022 import java.util.Iterator; 023 import java.util.List; 024 import java.util.Map; 025 import java.util.Set; 026 027 import org.apache.commons.collections.map.LinkedMap; 028 import org.apache.commons.configuration.event.ConfigurationEvent; 029 import org.apache.commons.configuration.event.ConfigurationListener; 030 import org.apache.commons.lang.StringUtils; 031 032 /** 033 * <p> 034 * A helper class used by <code>{@link PropertiesConfiguration}</code> to keep 035 * the layout of a properties file. 036 * </p> 037 * <p> 038 * Instances of this class are associated with a 039 * <code>PropertiesConfiguration</code> object. They are responsible for 040 * analyzing properties files and for extracting as much information about the 041 * file layout (e.g. empty lines, comments) as possible. When the properties 042 * file is written back again it should be close to the original. 043 * </p> 044 * <p> 045 * The <code>PropertiesConfigurationLayout</code> object associated with a 046 * <code>PropertiesConfiguration</code> object can be obtained using the 047 * <code>getLayout()</code> method of the configuration. Then the methods 048 * provided by this class can be used to alter the properties file's layout. 049 * </p> 050 * <p> 051 * Implementation note: This is a very simple implementation, which is far away 052 * from being perfect, i.e. the original layout of a properties file won't be 053 * reproduced in all cases. One limitation is that comments for multi-valued 054 * property keys are concatenated. Maybe this implementation can later be 055 * improved. 056 * </p> 057 * <p> 058 * To get an impression how this class works consider the following properties 059 * file: 060 * </p> 061 * <p> 062 * 063 * <pre> 064 * # A demo configuration file 065 * # for Demo App 1.42 066 * 067 * # Application name 068 * AppName=Demo App 069 * 070 * # Application vendor 071 * AppVendor=DemoSoft 072 * 073 * 074 * # GUI properties 075 * # Window Color 076 * windowColors=0xFFFFFF,0x000000 077 * 078 * # Include some setting 079 * include=settings.properties 080 * # Another vendor 081 * AppVendor=TestSoft 082 * </pre> 083 * 084 * </p> 085 * <p> 086 * For this example the following points are relevant: 087 * </p> 088 * <p> 089 * <ul> 090 * <li>The first two lines are set as header comment. The header comment is 091 * determined by the last blanc line before the first property definition.</li> 092 * <li>For the property <code>AppName</code> one comment line and one 093 * leading blanc line is stored.</li> 094 * <li>For the property <code>windowColors</code> two comment lines and two 095 * leading blanc lines are stored.</li> 096 * <li>Include files is something this class cannot deal with well. When saving 097 * the properties configuration back, the included properties are simply 098 * contained in the original file. The comment before the include property is 099 * skipped.</li> 100 * <li>For all properties except for <code>AppVendor</code> the "single 101 * line" flag is set. This is relevant only for <code>windowColors</code>, 102 * which has multiple values defined in one line using the separator character.</li> 103 * <li>The <code>AppVendor</code> property appears twice. The comment lines 104 * are concatenated, so that <code>layout.getComment("AppVendor");</code> will 105 * result in <code>Application vendor<CR>Another vendor</code>, whith 106 * <code><CR></code> meaning the line separator. In addition the 107 * "single line" flag is set to <b>false</b> for this property. When 108 * the file is saved, two property definitions will be written (in series).</li> 109 * </ul> 110 * </p> 111 * 112 * @author <a 113 * href="http://commons.apache.org/configuration/team-list.html">Commons 114 * Configuration team</a> 115 * @version $Id: PropertiesConfigurationLayout.java 589380 2007-10-28 16:37:35Z oheger $ 116 * @since 1.3 117 */ 118 public class PropertiesConfigurationLayout implements ConfigurationListener 119 { 120 /** Constant for the line break character. */ 121 private static final String CR = System.getProperty("line.separator"); 122 123 /** Constant for the default comment prefix. */ 124 private static final String COMMENT_PREFIX = "# "; 125 126 /** Stores the associated configuration object. */ 127 private PropertiesConfiguration configuration; 128 129 /** Stores a map with the contained layout information. */ 130 private Map layoutData; 131 132 /** Stores the header comment. */ 133 private String headerComment; 134 135 /** A counter for determining nested load calls. */ 136 private int loadCounter; 137 138 /** Stores the force single line flag. */ 139 private boolean forceSingleLine; 140 141 /** 142 * Creates a new instance of <code>PropertiesConfigurationLayout</code> 143 * and initializes it with the associated configuration object. 144 * 145 * @param config the configuration (must not be <b>null</b>) 146 */ 147 public PropertiesConfigurationLayout(PropertiesConfiguration config) 148 { 149 this(config, null); 150 } 151 152 /** 153 * Creates a new instance of <code>PropertiesConfigurationLayout</code> 154 * and initializes it with the given configuration object. The data of the 155 * specified layout object is copied. 156 * 157 * @param config the configuration (must not be <b>null</b>) 158 * @param c the layout object to be copied 159 */ 160 public PropertiesConfigurationLayout(PropertiesConfiguration config, 161 PropertiesConfigurationLayout c) 162 { 163 if (config == null) 164 { 165 throw new IllegalArgumentException( 166 "Configuration must not be null!"); 167 } 168 configuration = config; 169 layoutData = new LinkedMap(); 170 config.addConfigurationListener(this); 171 172 if (c != null) 173 { 174 copyFrom(c); 175 } 176 } 177 178 /** 179 * Returns the associated configuration object. 180 * 181 * @return the associated configuration 182 */ 183 public PropertiesConfiguration getConfiguration() 184 { 185 return configuration; 186 } 187 188 /** 189 * Returns the comment for the specified property key in a cononical form. 190 * "Canonical" means that either all lines start with a comment 191 * character or none. The <code>commentChar</code> parameter is <b>false</b>, 192 * all comment characters are removed, so that the result is only the plain 193 * text of the comment. Otherwise it is ensured that each line of the 194 * comment starts with a comment character. 195 * 196 * @param key the key of the property 197 * @param commentChar determines whether all lines should start with comment 198 * characters or not 199 * @return the canonical comment for this key (can be <b>null</b>) 200 */ 201 public String getCanonicalComment(String key, boolean commentChar) 202 { 203 String comment = getComment(key); 204 if (comment == null) 205 { 206 return null; 207 } 208 else 209 { 210 return trimComment(comment, commentChar); 211 } 212 } 213 214 /** 215 * Returns the comment for the specified property key. The comment is 216 * returned as it was set (either manually by calling 217 * <code>setComment()</code> or when it was loaded from a properties 218 * file). No modifications are performed. 219 * 220 * @param key the key of the property 221 * @return the comment for this key (can be <b>null</b>) 222 */ 223 public String getComment(String key) 224 { 225 return fetchLayoutData(key).getComment(); 226 } 227 228 /** 229 * Sets the comment for the specified property key. The comment (or its 230 * single lines if it is a multi-line comment) can start with a comment 231 * character. If this is the case, it will be written without changes. 232 * Otherwise a default comment character is added automatically. 233 * 234 * @param key the key of the property 235 * @param comment the comment for this key (can be <b>null</b>, then the 236 * comment will be removed) 237 */ 238 public void setComment(String key, String comment) 239 { 240 fetchLayoutData(key).setComment(comment); 241 } 242 243 /** 244 * Returns the number of blanc lines before this property key. If this key 245 * does not exist, 0 will be returned. 246 * 247 * @param key the property key 248 * @return the number of blanc lines before the property definition for this 249 * key 250 */ 251 public int getBlancLinesBefore(String key) 252 { 253 return fetchLayoutData(key).getBlancLines(); 254 } 255 256 /** 257 * Sets the number of blanc lines before the given property key. This can be 258 * used for a logical grouping of properties. 259 * 260 * @param key the property key 261 * @param number the number of blanc lines to add before this property 262 * definition 263 */ 264 public void setBlancLinesBefore(String key, int number) 265 { 266 fetchLayoutData(key).setBlancLines(number); 267 } 268 269 /** 270 * Returns the header comment of the represented properties file in a 271 * canonical form. With the <code>commentChar</code> parameter it can be 272 * specified whether comment characters should be stripped or be always 273 * present. 274 * 275 * @param commentChar determines the presence of comment characters 276 * @return the header comment (can be <b>null</b>) 277 */ 278 public String getCanonicalHeaderComment(boolean commentChar) 279 { 280 return (getHeaderComment() == null) ? null : trimComment( 281 getHeaderComment(), commentChar); 282 } 283 284 /** 285 * Returns the header comment of the represented properties file. This 286 * method returns the header comment exactly as it was set using 287 * <code>setHeaderComment()</code> or extracted from the loaded properties 288 * file. 289 * 290 * @return the header comment (can be <b>null</b>) 291 */ 292 public String getHeaderComment() 293 { 294 return headerComment; 295 } 296 297 /** 298 * Sets the header comment for the represented properties file. This comment 299 * will be output on top of the file. 300 * 301 * @param comment the comment 302 */ 303 public void setHeaderComment(String comment) 304 { 305 headerComment = comment; 306 } 307 308 /** 309 * Returns a flag whether the specified property is defined on a single 310 * line. This is meaningful only if this property has multiple values. 311 * 312 * @param key the property key 313 * @return a flag if this property is defined on a single line 314 */ 315 public boolean isSingleLine(String key) 316 { 317 return fetchLayoutData(key).isSingleLine(); 318 } 319 320 /** 321 * Sets the "single line flag" for the specified property key. 322 * This flag is evaluated if the property has multiple values (i.e. if it is 323 * a list property). In this case, if the flag is set, all values will be 324 * written in a single property definition using the list delimiter as 325 * separator. Otherwise multiple lines will be written for this property, 326 * each line containing one property value. 327 * 328 * @param key the property key 329 * @param f the single line flag 330 */ 331 public void setSingleLine(String key, boolean f) 332 { 333 fetchLayoutData(key).setSingleLine(f); 334 } 335 336 /** 337 * Returns the "force single line" flag. 338 * 339 * @return the force single line flag 340 * @see #setForceSingleLine(boolean) 341 */ 342 public boolean isForceSingleLine() 343 { 344 return forceSingleLine; 345 } 346 347 /** 348 * Sets the "force single line" flag. If this flag is set, all 349 * properties with multiple values are written on single lines. This mode 350 * provides more compatibility with <code>java.lang.Properties</code>, 351 * which cannot deal with multiple definitions of a single property. This 352 * mode has no effect if the list delimiter parsing is disabled. 353 * 354 * @param f the force single line flag 355 */ 356 public void setForceSingleLine(boolean f) 357 { 358 forceSingleLine = f; 359 } 360 361 /** 362 * Returns a set with all property keys managed by this object. 363 * 364 * @return a set with all contained property keys 365 */ 366 public Set getKeys() 367 { 368 return layoutData.keySet(); 369 } 370 371 /** 372 * Reads a properties file and stores its internal structure. The found 373 * properties will be added to the associated configuration object. 374 * 375 * @param in the reader to the properties file 376 * @throws ConfigurationException if an error occurs 377 */ 378 public void load(Reader in) throws ConfigurationException 379 { 380 if (++loadCounter == 1) 381 { 382 getConfiguration().removeConfigurationListener(this); 383 } 384 PropertiesConfiguration.PropertiesReader reader = new PropertiesConfiguration.PropertiesReader( 385 in, getConfiguration().getListDelimiter()); 386 387 try 388 { 389 while (reader.nextProperty()) 390 { 391 if (getConfiguration().propertyLoaded(reader.getPropertyName(), 392 reader.getPropertyValue())) 393 { 394 boolean contained = layoutData.containsKey(reader 395 .getPropertyName()); 396 int blancLines = 0; 397 int idx = checkHeaderComment(reader.getCommentLines()); 398 while (idx < reader.getCommentLines().size() 399 && ((String) reader.getCommentLines().get(idx)) 400 .length() < 1) 401 { 402 idx++; 403 blancLines++; 404 } 405 String comment = extractComment(reader.getCommentLines(), 406 idx, reader.getCommentLines().size() - 1); 407 PropertyLayoutData data = fetchLayoutData(reader 408 .getPropertyName()); 409 if (contained) 410 { 411 data.addComment(comment); 412 data.setSingleLine(false); 413 } 414 else 415 { 416 data.setComment(comment); 417 data.setBlancLines(blancLines); 418 } 419 } 420 } 421 } 422 catch (IOException ioex) 423 { 424 throw new ConfigurationException(ioex); 425 } 426 finally 427 { 428 if (--loadCounter == 0) 429 { 430 getConfiguration().addConfigurationListener(this); 431 } 432 } 433 } 434 435 /** 436 * Writes the properties file to the given writer, preserving as much of its 437 * structure as possible. 438 * 439 * @param out the writer 440 * @throws ConfigurationException if an error occurs 441 */ 442 public void save(Writer out) throws ConfigurationException 443 { 444 try 445 { 446 char delimiter = getConfiguration().isDelimiterParsingDisabled() ? 0 447 : getConfiguration().getListDelimiter(); 448 PropertiesConfiguration.PropertiesWriter writer = new PropertiesConfiguration.PropertiesWriter( 449 out, delimiter); 450 if (headerComment != null) 451 { 452 writer.writeln(getCanonicalHeaderComment(true)); 453 writer.writeln(null); 454 } 455 456 for (Iterator it = layoutData.keySet().iterator(); it.hasNext();) 457 { 458 String key = (String) it.next(); 459 if (getConfiguration().containsKey(key)) 460 { 461 462 // Output blank lines before property 463 for (int i = 0; i < getBlancLinesBefore(key); i++) 464 { 465 writer.writeln(null); 466 } 467 468 // Output the comment 469 if (getComment(key) != null) 470 { 471 writer.writeln(getCanonicalComment(key, true)); 472 } 473 474 // Output the property and its value 475 boolean singleLine = (isForceSingleLine() || isSingleLine(key)) 476 && !getConfiguration().isDelimiterParsingDisabled(); 477 writer.writeProperty(key, getConfiguration().getProperty( 478 key), singleLine); 479 } 480 } 481 writer.flush(); 482 } 483 catch (IOException ioex) 484 { 485 throw new ConfigurationException(ioex); 486 } 487 } 488 489 /** 490 * The event listener callback. Here event notifications of the 491 * configuration object are processed to update the layout object properly. 492 * 493 * @param event the event object 494 */ 495 public void configurationChanged(ConfigurationEvent event) 496 { 497 if (event.isBeforeUpdate()) 498 { 499 if (AbstractFileConfiguration.EVENT_RELOAD == event.getType()) 500 { 501 clear(); 502 } 503 } 504 505 else 506 { 507 switch (event.getType()) 508 { 509 case AbstractConfiguration.EVENT_ADD_PROPERTY: 510 boolean contained = layoutData.containsKey(event 511 .getPropertyName()); 512 PropertyLayoutData data = fetchLayoutData(event 513 .getPropertyName()); 514 data.setSingleLine(!contained); 515 break; 516 case AbstractConfiguration.EVENT_CLEAR_PROPERTY: 517 layoutData.remove(event.getPropertyName()); 518 break; 519 case AbstractConfiguration.EVENT_CLEAR: 520 clear(); 521 break; 522 case AbstractConfiguration.EVENT_SET_PROPERTY: 523 fetchLayoutData(event.getPropertyName()); 524 break; 525 } 526 } 527 } 528 529 /** 530 * Returns a layout data object for the specified key. If this is a new key, 531 * a new object is created and initialized with default values. 532 * 533 * @param key the key 534 * @return the corresponding layout data object 535 */ 536 private PropertyLayoutData fetchLayoutData(String key) 537 { 538 if (key == null) 539 { 540 throw new IllegalArgumentException("Property key must not be null!"); 541 } 542 543 PropertyLayoutData data = (PropertyLayoutData) layoutData.get(key); 544 if (data == null) 545 { 546 data = new PropertyLayoutData(); 547 data.setSingleLine(true); 548 layoutData.put(key, data); 549 } 550 551 return data; 552 } 553 554 /** 555 * Removes all content from this layout object. 556 */ 557 private void clear() 558 { 559 layoutData.clear(); 560 setHeaderComment(null); 561 } 562 563 /** 564 * Tests whether a line is a comment, i.e. whether it starts with a comment 565 * character. 566 * 567 * @param line the line 568 * @return a flag if this is a comment line 569 */ 570 static boolean isCommentLine(String line) 571 { 572 return PropertiesConfiguration.isCommentLine(line); 573 } 574 575 /** 576 * Trims a comment. This method either removes all comment characters from 577 * the given string, leaving only the plain comment text or ensures that 578 * every line starts with a valid comment character. 579 * 580 * @param s the string to be processed 581 * @param comment if <b>true</b>, a comment character will always be 582 * enforced; if <b>false</b>, it will be removed 583 * @return the trimmed comment 584 */ 585 static String trimComment(String s, boolean comment) 586 { 587 StringBuffer buf = new StringBuffer(s.length()); 588 int lastPos = 0; 589 int pos; 590 591 do 592 { 593 pos = s.indexOf(CR, lastPos); 594 if (pos >= 0) 595 { 596 String line = s.substring(lastPos, pos); 597 buf.append(stripCommentChar(line, comment)).append(CR); 598 lastPos = pos + CR.length(); 599 } 600 } while (pos >= 0); 601 602 if (lastPos < s.length()) 603 { 604 buf.append(stripCommentChar(s.substring(lastPos), comment)); 605 } 606 return buf.toString(); 607 } 608 609 /** 610 * Either removes the comment character from the given comment line or 611 * ensures that the line starts with a comment character. 612 * 613 * @param s the comment line 614 * @param comment if <b>true</b>, a comment character will always be 615 * enforced; if <b>false</b>, it will be removed 616 * @return the line without comment character 617 */ 618 static String stripCommentChar(String s, boolean comment) 619 { 620 if (s.length() < 1 || (isCommentLine(s) == comment)) 621 { 622 return s; 623 } 624 625 else 626 { 627 if (!comment) 628 { 629 int pos = 0; 630 // find first comment character 631 while (PropertiesConfiguration.COMMENT_CHARS.indexOf(s 632 .charAt(pos)) < 0) 633 { 634 pos++; 635 } 636 637 // Remove leading spaces 638 pos++; 639 while (pos < s.length() 640 && Character.isWhitespace(s.charAt(pos))) 641 { 642 pos++; 643 } 644 645 return (pos < s.length()) ? s.substring(pos) 646 : StringUtils.EMPTY; 647 } 648 else 649 { 650 return COMMENT_PREFIX + s; 651 } 652 } 653 } 654 655 /** 656 * Extracts a comment string from the given range of the specified comment 657 * lines. The single lines are added using a line feed as separator. 658 * 659 * @param commentLines a list with comment lines 660 * @param from the start index 661 * @param to the end index (inclusive) 662 * @return the comment string (<b>null</b> if it is undefined) 663 */ 664 private String extractComment(List commentLines, int from, int to) 665 { 666 if (to < from) 667 { 668 return null; 669 } 670 671 else 672 { 673 StringBuffer buf = new StringBuffer((String) commentLines.get(from)); 674 for (int i = from + 1; i <= to; i++) 675 { 676 buf.append(CR); 677 buf.append(commentLines.get(i)); 678 } 679 return buf.toString(); 680 } 681 } 682 683 /** 684 * Checks if parts of the passed in comment can be used as header comment. 685 * This method checks whether a header comment can be defined (i.e. whether 686 * this is the first comment in the loaded file). If this is the case, it is 687 * searched for the lates blanc line. This line will mark the end of the 688 * header comment. The return value is the index of the first line in the 689 * passed in list, which does not belong to the header comment. 690 * 691 * @param commentLines the comment lines 692 * @return the index of the next line after the header comment 693 */ 694 private int checkHeaderComment(List commentLines) 695 { 696 if (loadCounter == 1 && getHeaderComment() == null 697 && layoutData.isEmpty()) 698 { 699 // This is the first comment. Search for blanc lines. 700 int index = commentLines.size() - 1; 701 while (index >= 0 702 && ((String) commentLines.get(index)).length() > 0) 703 { 704 index--; 705 } 706 setHeaderComment(extractComment(commentLines, 0, index - 1)); 707 return index + 1; 708 } 709 else 710 { 711 return 0; 712 } 713 } 714 715 /** 716 * Copies the data from the given layout object. 717 * 718 * @param c the layout object to copy 719 */ 720 private void copyFrom(PropertiesConfigurationLayout c) 721 { 722 for (Iterator it = c.getKeys().iterator(); it.hasNext();) 723 { 724 String key = (String) it.next(); 725 PropertyLayoutData data = (PropertyLayoutData) c.layoutData 726 .get(key); 727 layoutData.put(key, data.clone()); 728 } 729 } 730 731 /** 732 * A helper class for storing all layout related information for a 733 * configuration property. 734 */ 735 static class PropertyLayoutData implements Cloneable 736 { 737 /** Stores the comment for the property. */ 738 private StringBuffer comment; 739 740 /** Stores the number of blanc lines before this property. */ 741 private int blancLines; 742 743 /** Stores the single line property. */ 744 private boolean singleLine; 745 746 /** 747 * Creates a new instance of <code>PropertyLayoutData</code>. 748 */ 749 public PropertyLayoutData() 750 { 751 singleLine = true; 752 } 753 754 /** 755 * Returns the number of blanc lines before this property. 756 * 757 * @return the number of blanc lines before this property 758 */ 759 public int getBlancLines() 760 { 761 return blancLines; 762 } 763 764 /** 765 * Sets the number of properties before this property. 766 * 767 * @param blancLines the number of properties before this property 768 */ 769 public void setBlancLines(int blancLines) 770 { 771 this.blancLines = blancLines; 772 } 773 774 /** 775 * Returns the single line flag. 776 * 777 * @return the single line flag 778 */ 779 public boolean isSingleLine() 780 { 781 return singleLine; 782 } 783 784 /** 785 * Sets the single line flag. 786 * 787 * @param singleLine the single line flag 788 */ 789 public void setSingleLine(boolean singleLine) 790 { 791 this.singleLine = singleLine; 792 } 793 794 /** 795 * Adds a comment for this property. If already a comment exists, the 796 * new comment is added (separated by a newline). 797 * 798 * @param s the comment to add 799 */ 800 public void addComment(String s) 801 { 802 if (s != null) 803 { 804 if (comment == null) 805 { 806 comment = new StringBuffer(s); 807 } 808 else 809 { 810 comment.append(CR).append(s); 811 } 812 } 813 } 814 815 /** 816 * Sets the comment for this property. 817 * 818 * @param s the new comment (can be <b>null</b>) 819 */ 820 public void setComment(String s) 821 { 822 if (s == null) 823 { 824 comment = null; 825 } 826 else 827 { 828 comment = new StringBuffer(s); 829 } 830 } 831 832 /** 833 * Returns the comment for this property. The comment is returned as it 834 * is, without processing of comment characters. 835 * 836 * @return the comment (can be <b>null</b>) 837 */ 838 public String getComment() 839 { 840 return (comment == null) ? null : comment.toString(); 841 } 842 843 /** 844 * Creates a copy of this object. 845 * 846 * @return the copy 847 */ 848 public Object clone() 849 { 850 try 851 { 852 PropertyLayoutData copy = (PropertyLayoutData) super.clone(); 853 if (comment != null) 854 { 855 // must copy string buffer, too 856 copy.comment = new StringBuffer(getComment()); 857 } 858 return copy; 859 } 860 catch (CloneNotSupportedException cnex) 861 { 862 // This cannot happen! 863 throw new ConfigurationRuntimeException(cnex); 864 } 865 } 866 } 867 }