PropertiesConfigurationLayout.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.configuration2;

  18. import java.io.IOException;
  19. import java.io.Reader;
  20. import java.io.Writer;
  21. import java.net.URL;
  22. import java.util.ArrayDeque;
  23. import java.util.LinkedHashMap;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Objects;
  27. import java.util.Properties;
  28. import java.util.Set;
  29. import java.util.concurrent.atomic.AtomicInteger;

  30. import org.apache.commons.configuration2.event.ConfigurationEvent;
  31. import org.apache.commons.configuration2.event.EventListener;
  32. import org.apache.commons.configuration2.ex.ConfigurationException;
  33. import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
  34. import org.apache.commons.lang3.StringUtils;

  35. /**
  36.  * <p>
  37.  * A helper class used by {@link PropertiesConfiguration} to keep the layout of a properties file.
  38.  * </p>
  39.  * <p>
  40.  * Instances of this class are associated with a {@code PropertiesConfiguration} object. They are responsible for
  41.  * analyzing properties files and for extracting as much information about the file layout (for example empty lines, comments)
  42.  * as possible. When the properties file is written back again it should be close to the original.
  43.  * </p>
  44.  * <p>
  45.  * The {@code PropertiesConfigurationLayout} object associated with a {@code PropertiesConfiguration} object can be
  46.  * obtained using the {@code getLayout()} method of the configuration. Then the methods provided by this class can be
  47.  * used to alter the properties file's layout.
  48.  * </p>
  49.  * <p>
  50.  * Implementation note: This is a very simple implementation, which is far away from being perfect, i.e. the original
  51.  * layout of a properties file won't be reproduced in all cases. One limitation is that comments for multi-valued
  52.  * property keys are concatenated. Maybe this implementation can later be improved.
  53.  * </p>
  54.  * <p>
  55.  * To get an impression how this class works consider the following properties file:
  56.  * </p>
  57.  *
  58.  * <pre>
  59.  * # A demo configuration file
  60.  * # for Demo App 1.42
  61.  *
  62.  * # Application name
  63.  * AppName=Demo App
  64.  *
  65.  * # Application vendor
  66.  * AppVendor=DemoSoft
  67.  *
  68.  *
  69.  * # GUI properties
  70.  * # Window Color
  71.  * windowColors=0xFFFFFF,0x000000
  72.  *
  73.  * # Include some setting
  74.  * include=settings.properties
  75.  * # Another vendor
  76.  * AppVendor=TestSoft
  77.  * </pre>
  78.  *
  79.  * <p>
  80.  * For this example the following points are relevant:
  81.  * </p>
  82.  * <ul>
  83.  * <li>The first two lines are set as header comment. The header comment is determined by the last blank line before the
  84.  * first property definition.</li>
  85.  * <li>For the property {@code AppName} one comment line and one leading blank line is stored.</li>
  86.  * <li>For the property {@code windowColors} two comment lines and two leading blank lines are stored.</li>
  87.  * <li>Include files is something this class cannot deal with well. When saving the properties configuration back, the
  88.  * included properties are simply contained in the original file. The comment before the include property is
  89.  * skipped.</li>
  90.  * <li>For all properties except for {@code AppVendor} the &quot;single line&quot; flag is set. This is relevant only
  91.  * for {@code windowColors}, which has multiple values defined in one line using the separator character.</li>
  92.  * <li>The {@code AppVendor} property appears twice. The comment lines are concatenated, so that
  93.  * {@code layout.getComment("AppVendor");} will result in {@code Application vendor&lt;CR&gt;Another vendor}, with
  94.  * {@code &lt;CR&gt;} meaning the line separator. In addition the &quot;single line&quot; flag is set to <strong>false</strong>
  95.  * for this property. When the file is saved, two property definitions will be written (in series).</li>
  96.  * </ul>
  97.  *
  98.  * @since 1.3
  99.  */
  100. public class PropertiesConfigurationLayout implements EventListener<ConfigurationEvent> {
  101.     /**
  102.      * A helper class for storing all layout related information for a configuration property.
  103.      */
  104.     static class PropertyLayoutData implements Cloneable {
  105.         /** Stores the comment for the property. */
  106.         private StringBuffer comment;

  107.         /** The separator to be used for this property. */
  108.         private String separator;

  109.         /** Stores the number of blank lines before this property. */
  110.         private int blankLines;

  111.         /** Stores the single line property. */
  112.         private boolean singleLine;

  113.         /**
  114.          * Creates a new instance of {@code PropertyLayoutData}.
  115.          */
  116.         public PropertyLayoutData() {
  117.             singleLine = true;
  118.             separator = PropertiesConfiguration.DEFAULT_SEPARATOR;
  119.         }

  120.         /**
  121.          * Adds a comment for this property. If already a comment exists, the new comment is added (separated by a newline).
  122.          *
  123.          * @param s the comment to add
  124.          */
  125.         public void addComment(final String s) {
  126.             if (s != null) {
  127.                 if (comment == null) {
  128.                     comment = new StringBuffer(s);
  129.                 } else {
  130.                     comment.append(CR).append(s);
  131.                 }
  132.             }
  133.         }

  134.         /**
  135.          * Creates a copy of this object.
  136.          *
  137.          * @return the copy
  138.          */
  139.         @Override
  140.         public PropertyLayoutData clone() {
  141.             try {
  142.                 final PropertyLayoutData copy = (PropertyLayoutData) super.clone();
  143.                 if (comment != null) {
  144.                     // must copy string buffer, too
  145.                     copy.comment = new StringBuffer(getComment());
  146.                 }
  147.                 return copy;
  148.             } catch (final CloneNotSupportedException cnex) {
  149.                 // This cannot happen!
  150.                 throw new ConfigurationRuntimeException(cnex);
  151.             }
  152.         }

  153.         /**
  154.          * Gets the number of blank lines before this property.
  155.          *
  156.          * @return the number of blank lines before this property
  157.          * @deprecated Use {#link {@link #getBlankLines()}}.
  158.          */
  159.         @Deprecated
  160.         public int getBlancLines() {
  161.             return getBlankLines();
  162.         }

  163.         /**
  164.          * Gets the number of blank lines before this property.
  165.          *
  166.          * @return the number of blank lines before this property
  167.          * @since 2.8.0
  168.          */
  169.         public int getBlankLines() {
  170.             return blankLines;
  171.         }

  172.         /**
  173.          * Gets the comment for this property. The comment is returned as it is, without processing of comment characters.
  174.          *
  175.          * @return the comment (can be <strong>null</strong>)
  176.          */
  177.         public String getComment() {
  178.             return Objects.toString(comment, null);
  179.         }

  180.         /**
  181.          * Gets the separator that was used for this property.
  182.          *
  183.          * @return the property separator
  184.          */
  185.         public String getSeparator() {
  186.             return separator;
  187.         }

  188.         /**
  189.          * Returns the single line flag.
  190.          *
  191.          * @return the single line flag
  192.          */
  193.         public boolean isSingleLine() {
  194.             return singleLine;
  195.         }

  196.         /**
  197.          * Sets the number of properties before this property.
  198.          *
  199.          * @param blankLines the number of properties before this property
  200.          * @deprecated Use {@link #setBlankLines(int)}.
  201.          */
  202.         @Deprecated
  203.         public void setBlancLines(final int blankLines) {
  204.             setBlankLines(blankLines);
  205.         }

  206.         /**
  207.          * Sets the number of properties before this property.
  208.          *
  209.          * @param blankLines the number of properties before this property
  210.          * @since 2.8.0
  211.          */
  212.         public void setBlankLines(final int blankLines) {
  213.             this.blankLines = blankLines;
  214.         }

  215.         /**
  216.          * Sets the comment for this property.
  217.          *
  218.          * @param s the new comment (can be <strong>null</strong>)
  219.          */
  220.         public void setComment(final String s) {
  221.             if (s == null) {
  222.                 comment = null;
  223.             } else {
  224.                 comment = new StringBuffer(s);
  225.             }
  226.         }

  227.         /**
  228.          * Sets the separator to be used for the represented property.
  229.          *
  230.          * @param separator the property separator
  231.          */
  232.         public void setSeparator(final String separator) {
  233.             this.separator = separator;
  234.         }

  235.         /**
  236.          * Sets the single line flag.
  237.          *
  238.          * @param singleLine the single line flag
  239.          */
  240.         public void setSingleLine(final boolean singleLine) {
  241.             this.singleLine = singleLine;
  242.         }
  243.     }

  244.     /** Constant for the line break character. */
  245.     private static final String CR = "\n";

  246.     /** Constant for the default comment prefix. */
  247.     private static final String COMMENT_PREFIX = "# ";

  248.     /**
  249.      * Helper method for generating a comment string. Depending on the boolean argument the resulting string either has no
  250.      * comment characters or a leading comment character at each line.
  251.      *
  252.      * @param comment the comment string to be processed
  253.      * @param commentChar determines the presence of comment characters
  254.      * @return the canonical comment string (can be <strong>null</strong>)
  255.      */
  256.     private static String constructCanonicalComment(final String comment, final boolean commentChar) {
  257.         return comment == null ? null : trimComment(comment, commentChar);
  258.     }

  259.     /**
  260.      * Tests whether a line is a comment, i.e. whether it starts with a comment character.
  261.      *
  262.      * @param line the line
  263.      * @return a flag if this is a comment line
  264.      */
  265.     static boolean isCommentLine(final String line) {
  266.         return PropertiesConfiguration.isCommentLine(line);
  267.     }

  268.     /**
  269.      * Either removes the comment character from the given comment line or ensures that the line starts with a comment
  270.      * character.
  271.      *
  272.      * @param s the comment line
  273.      * @param comment if <strong>true</strong>, a comment character will always be enforced; if <strong>false</strong>, it will be removed
  274.      * @return the line without comment character
  275.      */
  276.     static String stripCommentChar(final String s, final boolean comment) {
  277.         if (StringUtils.isBlank(s) || isCommentLine(s) == comment) {
  278.             return s;
  279.         }
  280.         if (!comment) {
  281.             int pos = 0;
  282.             // find first comment character
  283.             while (PropertiesConfiguration.COMMENT_CHARS.indexOf(s.charAt(pos)) < 0) {
  284.                 pos++;
  285.             }

  286.             // Remove leading spaces
  287.             pos++;
  288.             while (pos < s.length() && Character.isWhitespace(s.charAt(pos))) {
  289.                 pos++;
  290.             }

  291.             return pos < s.length() ? s.substring(pos) : StringUtils.EMPTY;
  292.         }
  293.         return COMMENT_PREFIX + s;
  294.     }

  295.     /**
  296.      * Trims a comment. This method either removes all comment characters from the given string, leaving only the plain
  297.      * comment text or ensures that every line starts with a valid comment character.
  298.      *
  299.      * @param s the string to be processed
  300.      * @param comment if <strong>true</strong>, a comment character will always be enforced; if <strong>false</strong>, it will be removed
  301.      * @return the trimmed comment
  302.      */
  303.     static String trimComment(final String s, final boolean comment) {
  304.         final StringBuilder buf = new StringBuilder(s.length());
  305.         int lastPos = 0;
  306.         int pos;

  307.         do {
  308.             pos = s.indexOf(CR, lastPos);
  309.             if (pos >= 0) {
  310.                 final String line = s.substring(lastPos, pos);
  311.                 buf.append(stripCommentChar(line, comment)).append(CR);
  312.                 lastPos = pos + CR.length();
  313.             }
  314.         } while (pos >= 0);

  315.         if (lastPos < s.length()) {
  316.             buf.append(stripCommentChar(s.substring(lastPos), comment));
  317.         }
  318.         return buf.toString();
  319.     }

  320.     /**
  321.      * Helper method for writing a comment line. This method ensures that the correct line separator is used if the comment
  322.      * spans multiple lines.
  323.      *
  324.      * @param writer the writer
  325.      * @param comment the comment to write
  326.      * @throws IOException if an IO error occurs
  327.      */
  328.     private static void writeComment(final PropertiesConfiguration.PropertiesWriter writer, final String comment) throws IOException {
  329.         if (comment != null) {
  330.             writer.writeln(StringUtils.replace(comment, CR, writer.getLineSeparator()));
  331.         }
  332.     }

  333.     /** Stores a map with the contained layout information. */
  334.     private final Map<String, PropertyLayoutData> layoutData;

  335.     /** Stores the header comment. */
  336.     private String headerComment;

  337.     /** Stores the footer comment. */
  338.     private String footerComment;

  339.     /** The global separator that will be used for all properties. */
  340.     private String globalSeparator;

  341.     /** The line separator. */
  342.     private String lineSeparator;

  343.     /** A counter for determining nested load calls. */
  344.     private final AtomicInteger loadCounter;

  345.     /** Stores the force single line flag. */
  346.     private boolean forceSingleLine;

  347.     /** Seen includes. */
  348.     private final ArrayDeque<URL> seenStack = new ArrayDeque<>();

  349.     /**
  350.      * Creates a new, empty instance of {@code PropertiesConfigurationLayout}.
  351.      */
  352.     public PropertiesConfigurationLayout() {
  353.         this(null);
  354.     }

  355.     /**
  356.      * Creates a new instance of {@code PropertiesConfigurationLayout} and copies the data of the specified layout object.
  357.      *
  358.      * @param c the layout object to be copied
  359.      */
  360.     public PropertiesConfigurationLayout(final PropertiesConfigurationLayout c) {
  361.         loadCounter = new AtomicInteger();
  362.         layoutData = new LinkedHashMap<>();

  363.         if (c != null) {
  364.             copyFrom(c);
  365.         }
  366.     }

  367.     /**
  368.      * Checks if parts of the passed in comment can be used as header comment. This method checks whether a header comment
  369.      * can be defined (i.e. whether this is the first comment in the loaded file). If this is the case, it is searched for
  370.      * the latest blank line. This line will mark the end of the header comment. The return value is the index of the first
  371.      * line in the passed in list, which does not belong to the header comment.
  372.      *
  373.      * @param commentLines the comment lines
  374.      * @return the index of the next line after the header comment
  375.      */
  376.     private int checkHeaderComment(final List<String> commentLines) {
  377.         if (loadCounter.get() == 1 && layoutData.isEmpty()) {
  378.             int index = commentLines.size() - 1;
  379.             // strip comments that belong to first key
  380.             while (index >= 0 && StringUtils.isNotEmpty(commentLines.get(index))) {
  381.                 index--;
  382.             }
  383.             // strip blank lines
  384.             while (index >= 0 && StringUtils.isEmpty(commentLines.get(index))) {
  385.                 index--;
  386.             }
  387.             if (getHeaderComment() == null) {
  388.                 setHeaderComment(extractComment(commentLines, 0, index));
  389.             }
  390.             return index + 1;
  391.         }
  392.         return 0;
  393.     }

  394.     /**
  395.      * Removes all content from this layout object.
  396.      */
  397.     private void clear() {
  398.         seenStack.clear();
  399.         layoutData.clear();
  400.         setHeaderComment(null);
  401.         setFooterComment(null);
  402.     }

  403.     /**
  404.      * Copies the data from the given layout object.
  405.      *
  406.      * @param c the layout object to copy
  407.      */
  408.     private void copyFrom(final PropertiesConfigurationLayout c) {
  409.         c.getKeys().forEach(key -> layoutData.put(key, c.layoutData.get(key).clone()));

  410.         setHeaderComment(c.getHeaderComment());
  411.         setFooterComment(c.getFooterComment());
  412.     }

  413.     /**
  414.      * Extracts a comment string from the given range of the specified comment lines. The single lines are added using a
  415.      * line feed as separator.
  416.      *
  417.      * @param commentLines a list with comment lines
  418.      * @param from the start index
  419.      * @param to the end index (inclusive)
  420.      * @return the comment string (<strong>null</strong> if it is undefined)
  421.      */
  422.     private String extractComment(final List<String> commentLines, final int from, final int to) {
  423.         if (to < from) {
  424.             return null;
  425.         }
  426.         final StringBuilder buf = new StringBuilder(commentLines.get(from));
  427.         for (int i = from + 1; i <= to; i++) {
  428.             buf.append(CR);
  429.             buf.append(commentLines.get(i));
  430.         }
  431.         return buf.toString();
  432.     }

  433.     /**
  434.      * Returns a layout data object for the specified key. If this is a new key, a new object is created and initialized
  435.      * with default values.
  436.      *
  437.      * @param key the key
  438.      * @return the corresponding layout data object
  439.      */
  440.     private PropertyLayoutData fetchLayoutData(final String key) {
  441.         if (key == null) {
  442.             throw new IllegalArgumentException("Property key must not be null!");
  443.         }

  444.         // PropertyLayoutData defaults to singleLine = true
  445.         return layoutData.computeIfAbsent(key, k -> new PropertyLayoutData());
  446.     }

  447.     /**
  448.      * Gets the number of blank lines before this property key. If this key does not exist, 0 will be returned.
  449.      *
  450.      * @param key the property key
  451.      * @return the number of blank lines before the property definition for this key
  452.      * @deprecated Use {@link #getBlankLinesBefore(String)}.
  453.      */
  454.     @Deprecated
  455.     public int getBlancLinesBefore(final String key) {
  456.         return getBlankLinesBefore(key);
  457.     }

  458.     /**
  459.      * Gets the number of blank lines before this property key. If this key does not exist, 0 will be returned.
  460.      *
  461.      * @param key the property key
  462.      * @return the number of blank lines before the property definition for this key
  463.      */
  464.     public int getBlankLinesBefore(final String key) {
  465.         return fetchLayoutData(key).getBlankLines();
  466.     }

  467.     /**
  468.      * Gets the comment for the specified property key in a canonical form. &quot;Canonical&quot; means that either all
  469.      * lines start with a comment character or none. If the {@code commentChar} parameter is <strong>false</strong>, all comment
  470.      * characters are removed, so that the result is only the plain text of the comment. Otherwise it is ensured that each
  471.      * line of the comment starts with a comment character. Also, line breaks in the comment are normalized to the line
  472.      * separator &quot;\n&quot;.
  473.      *
  474.      * @param key the key of the property
  475.      * @param commentChar determines whether all lines should start with comment characters or not
  476.      * @return the canonical comment for this key (can be <strong>null</strong>)
  477.      */
  478.     public String getCanonicalComment(final String key, final boolean commentChar) {
  479.         return constructCanonicalComment(getComment(key), commentChar);
  480.     }

  481.     /**
  482.      * Gets the footer comment of the represented properties file in a canonical form. This method works like
  483.      * {@code getCanonicalHeaderComment()}, but reads the footer comment.
  484.      *
  485.      * @param commentChar determines the presence of comment characters
  486.      * @return the footer comment (can be <strong>null</strong>)
  487.      * @see #getCanonicalHeaderComment(boolean)
  488.      * @since 2.0
  489.      */
  490.     public String getCanonicalFooterCooment(final boolean commentChar) {
  491.         return constructCanonicalComment(getFooterComment(), commentChar);
  492.     }

  493.     /**
  494.      * Gets the header comment of the represented properties file in a canonical form. With the {@code commentChar}
  495.      * parameter it can be specified whether comment characters should be stripped or be always present.
  496.      *
  497.      * @param commentChar determines the presence of comment characters
  498.      * @return the header comment (can be <strong>null</strong>)
  499.      */
  500.     public String getCanonicalHeaderComment(final boolean commentChar) {
  501.         return constructCanonicalComment(getHeaderComment(), commentChar);
  502.     }

  503.     /**
  504.      * Gets the comment for the specified property key. The comment is returned as it was set (either manually by calling
  505.      * {@code setComment()} or when it was loaded from a properties file). No modifications are performed.
  506.      *
  507.      * @param key the key of the property
  508.      * @return the comment for this key (can be <strong>null</strong>)
  509.      */
  510.     public String getComment(final String key) {
  511.         return fetchLayoutData(key).getComment();
  512.     }

  513.     /**
  514.      * Gets the footer comment of the represented properties file. This method returns the footer comment exactly as it
  515.      * was set using {@code setFooterComment()} or extracted from the loaded properties file.
  516.      *
  517.      * @return the footer comment (can be <strong>null</strong>)
  518.      * @since 2.0
  519.      */
  520.     public String getFooterComment() {
  521.         return footerComment;
  522.     }

  523.     /**
  524.      * Gets the global separator.
  525.      *
  526.      * @return the global properties separator
  527.      * @since 1.7
  528.      */
  529.     public String getGlobalSeparator() {
  530.         return globalSeparator;
  531.     }

  532.     /**
  533.      * Gets the header comment of the represented properties file. This method returns the header comment exactly as it
  534.      * was set using {@code setHeaderComment()} or extracted from the loaded properties file.
  535.      *
  536.      * @return the header comment (can be <strong>null</strong>)
  537.      */
  538.     public String getHeaderComment() {
  539.         return headerComment;
  540.     }

  541.     /**
  542.      * Gets a set with all property keys managed by this object.
  543.      *
  544.      * @return a set with all contained property keys
  545.      */
  546.     public Set<String> getKeys() {
  547.         return layoutData.keySet();
  548.     }

  549.     /**
  550.      * Gets the line separator.
  551.      *
  552.      * @return the line separator
  553.      * @since 1.7
  554.      */
  555.     public String getLineSeparator() {
  556.         return lineSeparator;
  557.     }

  558.     /**
  559.      * Gets the separator for the property with the given key.
  560.      *
  561.      * @param key the property key
  562.      * @return the property separator for this property
  563.      * @since 1.7
  564.      */
  565.     public String getSeparator(final String key) {
  566.         return fetchLayoutData(key).getSeparator();
  567.     }

  568.     /**
  569.      * Returns the &quot;force single line&quot; flag.
  570.      *
  571.      * @return the force single line flag
  572.      * @see #setForceSingleLine(boolean)
  573.      */
  574.     public boolean isForceSingleLine() {
  575.         return forceSingleLine;
  576.     }

  577.     /**
  578.      * Returns a flag whether the specified property is defined on a single line. This is meaningful only if this property
  579.      * has multiple values.
  580.      *
  581.      * @param key the property key
  582.      * @return a flag if this property is defined on a single line
  583.      */
  584.     public boolean isSingleLine(final String key) {
  585.         return fetchLayoutData(key).isSingleLine();
  586.     }

  587.     /**
  588.      * Reads a properties file and stores its internal structure. The found properties will be added to the specified
  589.      * configuration object.
  590.      *
  591.      * @param config the associated configuration object
  592.      * @param reader the reader to the properties file
  593.      * @throws ConfigurationException if an error occurs
  594.      */
  595.     public void load(final PropertiesConfiguration config, final Reader reader) throws ConfigurationException {
  596.         loadCounter.incrementAndGet();
  597.         @SuppressWarnings("resource") // createPropertiesReader wraps the reader.
  598.         final PropertiesConfiguration.PropertiesReader pReader = config.getIOFactory().createPropertiesReader(reader);

  599.         try {
  600.             while (pReader.nextProperty()) {
  601.                 if (config.propertyLoaded(pReader.getPropertyName(), pReader.getPropertyValue(), seenStack)) {
  602.                     final boolean contained = layoutData.containsKey(pReader.getPropertyName());
  603.                     int blankLines = 0;
  604.                     int idx = checkHeaderComment(pReader.getCommentLines());
  605.                     while (idx < pReader.getCommentLines().size() && StringUtils.isEmpty(pReader.getCommentLines().get(idx))) {
  606.                         idx++;
  607.                         blankLines++;
  608.                     }
  609.                     final String comment = extractComment(pReader.getCommentLines(), idx, pReader.getCommentLines().size() - 1);
  610.                     final PropertyLayoutData data = fetchLayoutData(pReader.getPropertyName());
  611.                     if (contained) {
  612.                         data.addComment(comment);
  613.                         data.setSingleLine(false);
  614.                     } else {
  615.                         data.setComment(comment);
  616.                         data.setBlankLines(blankLines);
  617.                         data.setSeparator(pReader.getPropertySeparator());
  618.                     }
  619.                 }
  620.             }

  621.             setFooterComment(extractComment(pReader.getCommentLines(), 0, pReader.getCommentLines().size() - 1));
  622.         } catch (final IOException ioex) {
  623.             throw new ConfigurationException(ioex);
  624.         } finally {
  625.             loadCounter.decrementAndGet();
  626.         }
  627.     }

  628.     /**
  629.      * The event listener callback. Here event notifications of the configuration object are processed to update the layout
  630.      * object properly.
  631.      *
  632.      * @param event the event object
  633.      */
  634.     @Override
  635.     public void onEvent(final ConfigurationEvent event) {
  636.         if (!event.isBeforeUpdate() && loadCounter.get() == 0) {
  637.             if (ConfigurationEvent.ADD_PROPERTY.equals(event.getEventType())) {
  638.                 final boolean contained = layoutData.containsKey(event.getPropertyName());
  639.                 final PropertyLayoutData data = fetchLayoutData(event.getPropertyName());
  640.                 data.setSingleLine(!contained);
  641.             } else if (ConfigurationEvent.CLEAR_PROPERTY.equals(event.getEventType())) {
  642.                 layoutData.remove(event.getPropertyName());
  643.             } else if (ConfigurationEvent.CLEAR.equals(event.getEventType())) {
  644.                 clear();
  645.             } else if (ConfigurationEvent.SET_PROPERTY.equals(event.getEventType())) {
  646.                 fetchLayoutData(event.getPropertyName());
  647.             }
  648.         }
  649.     }

  650.     /**
  651.      * Writes the properties file to the given writer, preserving as much of its structure as possible.
  652.      *
  653.      * @param config the associated configuration object
  654.      * @param writer the writer
  655.      * @throws ConfigurationException if an error occurs
  656.      */
  657.     public void save(final PropertiesConfiguration config, final Writer writer) throws ConfigurationException {
  658.         try {
  659.             @SuppressWarnings("resource") // createPropertiesReader wraps the writer.
  660.             final PropertiesConfiguration.PropertiesWriter pWriter = config.getIOFactory().createPropertiesWriter(writer, config.getListDelimiterHandler());
  661.             pWriter.setGlobalSeparator(getGlobalSeparator());
  662.             if (getLineSeparator() != null) {
  663.                 pWriter.setLineSeparator(getLineSeparator());
  664.             }

  665.             if (headerComment != null) {
  666.                 writeComment(pWriter, getCanonicalHeaderComment(true));
  667.             }

  668.             boolean firstKey = true;
  669.             for (final String key : getKeys()) {
  670.                 if (config.containsKeyInternal(key)) {
  671.                     // preset header comment needs to be separated from key
  672.                     if (firstKey && headerComment != null && getBlankLinesBefore(key) == 0) {
  673.                         pWriter.writeln(null);
  674.                     }

  675.                     // Output blank lines before property
  676.                     for (int i = 0; i < getBlankLinesBefore(key); i++) {
  677.                         pWriter.writeln(null);
  678.                     }

  679.                     // Output the comment
  680.                     writeComment(pWriter, getCanonicalComment(key, true));

  681.                     // Output the property and its value
  682.                     final boolean singleLine = isForceSingleLine() || isSingleLine(key);
  683.                     pWriter.setCurrentSeparator(getSeparator(key));
  684.                     pWriter.writeProperty(key, config.getPropertyInternal(key), singleLine);
  685.                 }
  686.                 firstKey = false;
  687.             }

  688.             writeComment(pWriter, getCanonicalFooterCooment(true));
  689.             pWriter.flush();
  690.         } catch (final IOException ioex) {
  691.             throw new ConfigurationException(ioex);
  692.         }
  693.     }

  694.     /**
  695.      * Sets the number of blank lines before the given property key. This can be used for a logical grouping of properties.
  696.      *
  697.      * @param key the property key
  698.      * @param number the number of blank lines to add before this property definition
  699.      * @deprecated use {@link PropertiesConfigurationLayout#setBlankLinesBefore(String, int)}.
  700.      */
  701.     @Deprecated
  702.     public void setBlancLinesBefore(final String key, final int number) {
  703.         setBlankLinesBefore(key, number);
  704.     }

  705.     /**
  706.      * Sets the number of blank lines before the given property key. This can be used for a logical grouping of properties.
  707.      *
  708.      * @param key the property key
  709.      * @param number the number of blank lines to add before this property definition
  710.      * @since 2.8.0
  711.      */
  712.     public void setBlankLinesBefore(final String key, final int number) {
  713.         fetchLayoutData(key).setBlankLines(number);
  714.     }

  715.     /**
  716.      * Sets the comment for the specified property key. The comment (or its single lines if it is a multi-line comment) can
  717.      * start with a comment character. If this is the case, it will be written without changes. Otherwise a default comment
  718.      * character is added automatically.
  719.      *
  720.      * @param key the key of the property
  721.      * @param comment the comment for this key (can be <strong>null</strong>, then the comment will be removed)
  722.      */
  723.     public void setComment(final String key, final String comment) {
  724.         fetchLayoutData(key).setComment(comment);
  725.     }

  726.     /**
  727.      * Sets the footer comment for the represented properties file. This comment will be output at the bottom of the file.
  728.      *
  729.      * @param footerComment the footer comment
  730.      * @since 2.0
  731.      */
  732.     public void setFooterComment(final String footerComment) {
  733.         this.footerComment = footerComment;
  734.     }

  735.     /**
  736.      * Sets the &quot;force single line&quot; flag. If this flag is set, all properties with multiple values are written on
  737.      * single lines. This mode provides more compatibility with {@link Properties}, which cannot deal with
  738.      * multiple definitions of a single property. This mode has no effect if the list delimiter parsing is disabled.
  739.      *
  740.      * @param f the force single line flag
  741.      */
  742.     public void setForceSingleLine(final boolean f) {
  743.         forceSingleLine = f;
  744.     }

  745.     /**
  746.      * Sets the global separator for properties. With this method a separator can be set that will be used for all
  747.      * properties when writing the configuration. This is an easy way of determining the properties separator globally. To
  748.      * be compatible with the properties format only the characters {@code =} and {@code :} (with or without whitespace)
  749.      * should be used, but this method does not enforce this - it accepts arbitrary strings. If the global separator is set
  750.      * to <strong>null</strong>, property separators are not changed. This is the default behavior as it produces results that are
  751.      * closer to the original properties file.
  752.      *
  753.      * @param globalSeparator the separator to be used for all properties
  754.      * @since 1.7
  755.      */
  756.     public void setGlobalSeparator(final String globalSeparator) {
  757.         this.globalSeparator = globalSeparator;
  758.     }

  759.     /**
  760.      * Sets the header comment for the represented properties file. This comment will be output on top of the file.
  761.      *
  762.      * @param comment the comment
  763.      */
  764.     public void setHeaderComment(final String comment) {
  765.         headerComment = comment;
  766.     }

  767.     /**
  768.      * Sets the line separator. When writing the properties configuration, all lines are terminated with this separator. If
  769.      * no separator was set, the platform-specific default line separator is used.
  770.      *
  771.      * @param lineSeparator the line separator
  772.      * @since 1.7
  773.      */
  774.     public void setLineSeparator(final String lineSeparator) {
  775.         this.lineSeparator = lineSeparator;
  776.     }

  777.     /**
  778.      * Sets the separator to be used for the property with the given key. The separator is the string between the property
  779.      * key and its value. For new properties &quot; = &quot; is used. When a properties file is read, the layout tries to
  780.      * determine the separator for each property. With this method the separator can be changed. To be compatible with the
  781.      * properties format only the characters {@code =} and {@code :} (with or without whitespace) should be used, but this
  782.      * method does not enforce this - it accepts arbitrary strings. If the key refers to a property with multiple values
  783.      * that are written on multiple lines, this separator will be used on all lines.
  784.      *
  785.      * @param key the key for the property
  786.      * @param sep the separator to be used for this property
  787.      * @since 1.7
  788.      */
  789.     public void setSeparator(final String key, final String sep) {
  790.         fetchLayoutData(key).setSeparator(sep);
  791.     }

  792.     /**
  793.      * Sets the &quot;single line flag&quot; for the specified property key. This flag is evaluated if the property has
  794.      * multiple values (i.e. if it is a list property). In this case, if the flag is set, all values will be written in a
  795.      * single property definition using the list delimiter as separator. Otherwise multiple lines will be written for this
  796.      * property, each line containing one property value.
  797.      *
  798.      * @param key the property key
  799.      * @param f the single line flag
  800.      */
  801.     public void setSingleLine(final String key, final boolean f) {
  802.         fetchLayoutData(key).setSingleLine(f);
  803.     }
  804. }