Question

I'm using java.util.Properties's store(Writer, String) method to store the properties. In the resulting text file, the properties are stored in a haphazard order.

This is what I'm doing:

Properties properties = createProperties();
properties.store(new FileWriter(file), null);

How can I ensure the properties are written out in alphabetical order, or in the order the properties were added?

I'm hoping for a solution simpler than "manually create the properties file".

Was it helpful?

Solution

As per "The New Idiot's" suggestion, this stores in alphabetical key order.

Properties tmp = new Properties() {
    @Override
    public synchronized Enumeration<Object> keys() {
        return Collections.enumeration(new TreeSet<Object>(super.keySet()));
    }
};
tmp.putAll(properties);
tmp.store(new FileWriter(file), null);

OTHER TIPS

See https://github.com/etiennestuder/java-ordered-properties for a complete implementation that allows to read/write properties files in a well-defined order.

OrderedProperties properties = new OrderedProperties();
properties.load(new FileInputStream(new File("~/some.properties")));

Steve McLeod's answer used to work for me, but since Java 11, it doesn't.

The problem seemed to be EntrySet ordering, so, here you go:

@SuppressWarnings("serial")
private static Properties newOrderedProperties() 
{
    return new Properties() {
        @Override public synchronized Set<Map.Entry<Object, Object>> entrySet() {
            return Collections.synchronizedSet(
                    super.entrySet()
                    .stream()
                    .sorted(Comparator.comparing(e -> e.getKey().toString()))
                    .collect(Collectors.toCollection(LinkedHashSet::new)));
        }
    };
}

I will warn that this is not fast by any means. It forces iteration over a LinkedHashSet which isn't ideal, but I'm open to suggestions.

To use a TreeSet is dangerous! Because in the CASE_INSENSITIVE_ORDER the strings "mykey", "MyKey" and "MYKEY" will result in the same index! (so 2 keys will be omitted).

I use List instead, to be sure to keep all keys.

 List<Object> list = new ArrayList<>( super.keySet());
 Comparator<Object> comparator = Comparator.comparing( Object::toString, String.CASE_INSENSITIVE_ORDER );
 Collections.sort( list, comparator );
 return Collections.enumeration( list );

The solution from Steve McLeod did not not work when trying to sort case insensitive.

This is what I came up with

Properties newProperties = new Properties() {

    private static final long serialVersionUID = 4112578634029874840L;

    @Override
    public synchronized Enumeration<Object> keys() {
        Comparator<Object> byCaseInsensitiveString = Comparator.comparing(Object::toString,
                        String.CASE_INSENSITIVE_ORDER);

        Supplier<TreeSet<Object>> supplier = () -> new TreeSet<>(byCaseInsensitiveString);

        TreeSet<Object> sortedSet = super.keySet().stream()
                        .collect(Collectors.toCollection(supplier));

        return Collections.enumeration(sortedSet);
    }
 };

    // propertyMap is a simple LinkedHashMap<String,String>
    newProperties.putAll(propertyMap);
    File file = new File(filepath);
    try (FileOutputStream fileOutputStream = new FileOutputStream(file, false)) {
        newProperties.store(fileOutputStream, null);
    }

In case someone has to do this in kotlin:

class OrderedProperties: Properties() {

    override val entries: MutableSet<MutableMap.MutableEntry<Any, Any>>
        get(){
            return Collections.synchronizedSet(
                super.entries
                    .stream()
                    .sorted(Comparator.comparing { e -> e.key.toString() })
                    .collect(
                        Collectors.toCollection(
                            Supplier { LinkedHashSet() })
                    )
            )
        }

}

I'm having the same itch, so I implemented a simple kludge subclass that allows you to explicitly pre-define the order name/values appear in one block and lexically order them in another block.

https://github.com/crums-io/io-util/blob/master/src/main/java/io/crums/util/TidyProperties.java

In any event, you need to override public Set<Map.Entry<Object, Object>> entrySet(), not public Enumeration<Object> keys(); the latter, as https://stackoverflow.com/users/704335/timmos points out, never hits on the store(..) method.

If your properties file is small, and you want a future-proof solution, then I suggest you to store the Properties object on a file and load the file back to a String (or store it to ByteArrayOutputStream and convert it to a String), split the string into lines, sort the lines, and write the lines to the destination file you want.

It's because the internal implementation of Properties class is always changing, and to achieve the sorting in store(), you need to override different methods of Properties class in different versions of Java (see How to sort Properties in java?). If your properties file is not large, then I prefer a future-proof solution over the best performance one.

For the correct way to split the string into lines, some reliable solutions are:

  • Files.lines()/Files.readAllLines(), if you use a File
  • BufferedReader.readLine() (Java 7 or earlier)
  • IOUtils.readLines(bufferedReader) (org.apache.commons.io.IOUtils, Java 7 or earlier)
  • BufferedReader.lines() (Java 8+) as mentioned in Split Java String by New Line
  • String.lines() (Java 11+) as mentioned in Split Java String by New Line.

And you don't need to be worried about values with multiple lines, because Properties.store() will escape the whole multi-line String into one line in the output file.

Sample codes for Java 8:

public static void test() {
    ......
    String comments = "Your multiline comments, this should be line 1." +
            "\n" +
            "The sorting should not mess up the comment lines' ordering, this should be line 2 even if T is smaller than Y";

    saveSortedPropertiesToFile(inputProperties, comments, Paths.get("C:\\dev\\sorted.properties"));
}

public static void saveSortedPropertiesToFile(Properties properties, String comments, Path destination) {
    try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
        // Storing it to output stream is the only way to make sure correct encoding is used.
        properties.store(outputStream, comments);

        /* The encoding here shouldn't matter, since you are not going to modify the contents,
           and you are only going to split them to lines and reorder them.
           And Properties.store(OutputStream, String) should have translated unicode characters into (backslash)uXXXX anyway.
         */
        String propertiesContentUnsorted = outputStream.toString("UTF-8");

        String propertiesContentSorted;
        try (BufferedReader bufferedReader = new BufferedReader(new StringReader(propertiesContentUnsorted))) {
            List<String> commentLines = new ArrayList<>();
            List<String> contentLines = new ArrayList<>();

            boolean commentSectionEnded = false;
            for (Iterator<String> it = bufferedReader.lines().iterator(); it.hasNext(); ) {
                String line = it.next();
                if (!commentSectionEnded) {
                    if (line.startsWith("#")) {
                        commentLines.add(line);
                    } else {
                        contentLines.add(line);
                        commentSectionEnded = true;
                    }
                } else {
                    contentLines.add(line);
                }
            }
            // Sort on content lines only
            propertiesContentSorted = Stream.concat(commentLines.stream(), contentLines.stream().sorted())
                    .collect(Collectors.joining(System.lineSeparator()));
        }

        // Just make sure you use the same encoding as above.
        Files.write(destination, propertiesContentSorted.getBytes(StandardCharsets.UTF_8));

    } catch (IOException e) {
        // Log it if necessary
    }
}

Sample codes for Java 7:

import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;

......

public static void test() {
    ......
    String comments = "Your multiline comments, this should be line 1." +
            "\n" +
            "The sorting should not mess up the comment lines' ordering, this should be line 2 even if T is smaller than Y";

    saveSortedPropertiesToFile(inputProperties, comments, Paths.get("C:\\dev\\sorted.properties"));
}

public static void saveSortedPropertiesToFile(Properties properties, String comments, Path destination) {
    try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
        // Storing it to output stream is the only way to make sure correct encoding is used.
        properties.store(outputStream, comments);

        /* The encoding here shouldn't matter, since you are not going to modify the contents,
           and you are only going to split them to lines and reorder them.
           And Properties.store(OutputStream, String) should have translated unicode characters into (backslash)uXXXX anyway.
         */
        String propertiesContentUnsorted = outputStream.toString("UTF-8");

        String propertiesContentSorted;
        try (BufferedReader bufferedReader = new BufferedReader(new StringReader(propertiesContentUnsorted))) {
            List<String> commentLines = new ArrayList<>();
            List<String> contentLines = new ArrayList<>();

            boolean commentSectionEnded = false;

            for (Iterator<String> it = IOUtils.readLines(bufferedReader).iterator(); it.hasNext(); ) {
                String line = it.next();
                if (!commentSectionEnded) {
                    if (line.startsWith("#")) {
                        commentLines.add(line);
                    } else {
                        contentLines.add(line);
                        commentSectionEnded = true;
                    }
                } else {
                    contentLines.add(line);
                }
            }
            // Sort on content lines only
            Collections.sort(contentLines);

            propertiesContentSorted = StringUtils.join(IterableUtils.chainedIterable(commentLines, contentLines).iterator(), System.lineSeparator());
        }

        // Just make sure you use the same encoding as above.
        Files.write(destination, propertiesContentSorted.getBytes(StandardCharsets.UTF_8));

    } catch (IOException e) {
        // Log it if necessary
    }
}

True that keys() is not triggered so instead of passing trough a list as Timmos suggested you can do it like this:

Properties alphaproperties = new Properties() {
        @Override
        public Set<Map.Entry<Object, Object>> entrySet() {
            Set<Map.Entry<Object, Object>> setnontrie = super.entrySet();
            Set<Map.Entry<Object, Object>> unSetTrie = new ConcurrentSkipListSet<Map.Entry<Object, Object>>(new Comparator<Map.Entry<Object, Object>>() {
                @Override
                public int compare(Map.Entry<Object, Object> o1, Map.Entry<Object, Object> o2) {
                    return o1.getKey().toString().compareTo(o2.getKey().toString());
                }
            });
            unSetTrie.addAll(setnontrie);
            return unSetTrie;
            }
    };
    alphaproperties.putAll(properties);
    alphaproperties.store(fw, "UpdatedBy Me");
    fw.close();
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top