32

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".

Steve McLeod
  • 51,737
  • 47
  • 128
  • 184

9 Answers9

65

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);
Steve McLeod
  • 51,737
  • 47
  • 128
  • 184
  • 2
    It will sort upper case first then lower case –  Nov 08 '13 at 05:39
  • I would prefer the answer from Etienne Studer, because if you want to have insertion order, you can't achieve to that on this way, because internally it already uses HashTable. In that case LinkedHashTable or LinkedHashSet wouldn't have any effect. – X-HuMan Oct 16 '15 at 08:30
  • 2
    I don't think this still works? A breakpoint in `keys()` doesn't trigger. I solved it by overriding the `entrySet()` method. Use `super.entrySet()`, convert to list, sort the list, convert back to Set and return. – Timmos Jan 21 '21 at 14:20
12

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")));
Etienne Studer
  • 301
  • 2
  • 3
  • Have you checked what do you store in the file at all? I have a feeling that the map which you used for the custom properties is not connected to the file at all and you store in the file the default HashTable, sorry if I am wrong. – X-HuMan Oct 14 '15 at 16:40
  • This is the test coverage: https://github.com/etiennestuder/java-ordered-properties/blob/master/src/test/groovy/nu/studer/java/util/OrderedPropertiesTest.groovy – Etienne Studer Oct 15 '15 at 17:41
  • Yes I see, thanks for the answer, I was trying to use this in Android, but actually java Properties has different implementation in Android, so you need to override entrySet() method as well to make it work. – X-HuMan Oct 16 '15 at 08:27
  • For those who did not see the link on github, the maven artifact is available here: https://bintray.com/etienne/java-utilities/java-ordered-properties – Michael_S Sep 17 '18 at 16:30
4

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.

CompEng88
  • 1,336
  • 14
  • 25
3

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);
    }
kevcodez
  • 1,261
  • 12
  • 27
  • line 2 & 3 of `keys()` method could be replaced by : `Set sortedKeys = new TreeSet<>(byCaseInsensitiveString);` followed by : `sortedKeys.addAll(super.keySet());` – Thierry Jun 04 '18 at 07:08
3

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 );
Acapulco
  • 3,373
  • 8
  • 38
  • 51
1

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.

Babak Farhang
  • 79
  • 1
  • 8
1

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() })
                    )
            )
        }

}
0

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
    }
}
sken130
  • 311
  • 2
  • 9
0

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();