Using ConfigurationProperties to fill Map in generic way

JavaSpringSpring Boot

Java Problem Overview


I'm wondering, if there is a generic way to fill a map with properties you just know the prefix.

Assuming there are a bunch of properties like

namespace.prop1=value1
namespace.prop2=value2
namespace.iDontKnowThisNameAtCompileTime=anothervalue

I'd like to have a generic way to fill this property inside a map, something like

@Component
@ConfigurationProperties("namespace")
public class MyGenericProps {
    private Map<String, String> propmap = new HashMap<String, String>();
    
    // setter and getter for propmap omitted

    public Set<String> returnAllKeys() {
        return propmap.keySet();
    }
}

Or is there another convenient way to collect all properties with a certain prefix, instead of iterating over all PropertySources in the environment?

Thanks Hansjoerg

Java Solutions


Solution 1 - Java

As long as you're happy having every property added into the map, rather than just those that you don't know in advance, you can do this with @ConfigurationProperties. If you want to grab everything that's beneath namespace then you need to use an empty prefix and provide a getter for a map named namespace:

@ConfigurationProperties("")
public class CustomProperties {
	
    private final Map<String, String> namespace = new HashMap<>();

	public Map<String, String> getNamespace() {
	    return namespace;
	}
	
}

Spring Boot uses the getNamespace method to retrieve the map so that it can add the properties to it. With these properties:

namespace.a=alpha
namespace.b=bravo
namespace.c=charlie

The namespace map will contain three entries:

{a=alpha, b=bravo, c=charlie}

If the properties were nested more deeply, for example:

namespace.foo.bar.a=alpha
namespace.foo.bar.b=bravo
namespace.foo.bar.c=charlie

Then you'd use namespace.foo as the prefix and rename namespace and getNamespace on CustomProperties to bar and getBar respectively.

Note that you should apply @EnableConfigurationProperties to your configuration to enable support for @ConfigurationProperties. You can then reference any beans that you want to be processed using that annotation, rather than providing an @Bean method for them, or using @Component to have them discovered by component scanning:

@SpringBootApplication
@EnableConfigurationProperties(CustomProperties.class)
public class YourApplication {
    // …
}
  

Solution 2 - Java

In addition to this, my problem was that I didn't had multiple simple key/value properties but whole objects:

zuul:
  routes:
    query1:
      path: /api/apps/test1/query/**
      stripPrefix: false
      url: "https://test.url.com/query1"
    query2:
       path: /api/apps/test2/query/**
       stripPrefix: false
       url: "https://test.url.com/query2"
    index1:
       path: /api/apps/*/index/**
       stripPrefix: false
       url: "https://test.url.com/index"

Following Jake's advice I tried to use a Map with a Pojo like this:

@ConfigurationProperties("zuul")
public class RouteConfig {
    private Map<String, Route> routes = new HashMap<>();

    public Map<String, Route> getRoutes() {
        return routes;
    }

    public static class Route {
        private String path;
        private boolean stripPrefix;
        String url;

        // [getters + setters]
    }
}

Works like a charm, Thanks!

Solution 3 - Java

I was going nuts trying to understand why @Andy's answer wasn't working for me (as in, the Map was remaining empty) just to realize that I had Lombok's @Builder annotation getting in the way, which added a non-empty constructor. I'm adding this answer to emphasize that in order for @ConfigurationProperties to work on Map, the value type must have a No-Arguments constructor. This is also mentioned in Spring's documentation:

> Such arrangement relies on a default empty constructor and getters and setters are usually mandatory ...

I hope this will save someone else some time.

Solution 4 - Java

I wrote myself a MapFilter class to handle this efficiently. Essentially, you create a Map and then filter it by specifying a prefix for the key. There is also a constructor that takes a Properties for convenience.

Be aware that this just filters the main map. Any changes applied to the filtered map are also applied to the base map, including deletions etc but obviously changes to the main map will not be reflected in the filtered map until something causes a rebuild.

It is also very easy (and efficient) to filter already filtered maps.

public class MapFilter<T> implements Map<String, T> {

    // The enclosed map -- could also be a MapFilter.
    final private Map<String, T> map;
    // Use a TreeMap for predictable iteration order.
    // Store Map.Entry to reflect changes down into the underlying map.
    // The Key is the shortened string. The entry.key is the full string.
    final private Map<String, Map.Entry<String, T>> entries = new TreeMap<>();
    // The prefix they are looking for in this map.
    final private String prefix;

    public MapFilter(Map<String, T> map, String prefix) {
        // Store my backing map.
        this.map = map;
        // Record my prefix.
        this.prefix = prefix;
        // Build my entries.
        rebuildEntries();
    }

    public MapFilter(Map<String, T> map) {
        this(map, "");
    }

    private synchronized void rebuildEntries() {
        // Start empty.
        entries.clear();
        // Build my entry set.
        for (Map.Entry<String, T> e : map.entrySet()) {
            String key = e.getKey();
            // Retain each one that starts with the specified prefix.
            if (key.startsWith(prefix)) {
                // Key it on the remainder.
                String k = key.substring(prefix.length());
                // Entries k always contains the LAST occurrence if there are multiples.
                entries.put(k, e);
            }
        }

    }

    @Override
    public String toString() {
        return "MapFilter (" + prefix + ") of " + map + " containing " + entrySet();
    }

    // Constructor from a properties file.
    public MapFilter(Properties p, String prefix) {
        // Properties extends HashTable<Object,Object> so it implements Map.
        // I need Map<String,T> so I wrap it in a HashMap for simplicity.
        // Java-8 breaks if we use diamond inference.
        this(new HashMap<String, T>((Map) p), prefix);
    }

    // Helper to fast filter the map.
    public MapFilter<T> filter(String prefix) {
        // Wrap me in a new filter.
        return new MapFilter<>(this, prefix);
    }

    // Count my entries.
    @Override
    public int size() {
        return entries.size();
    }

    // Are we empty.
    @Override
    public boolean isEmpty() {
        return entries.isEmpty();
    }

    // Is this key in me?
    @Override
    public boolean containsKey(Object key) {
        return entries.containsKey(key);
    }

    // Is this value in me.
    @Override
    public boolean containsValue(Object value) {
        // Walk the values.
        for (Map.Entry<String, T> e : entries.values()) {
            if (value.equals(e.getValue())) {
                // Its there!
                return true;
            }
        }
        return false;
    }

    // Get the referenced value - if present.
    @Override
    public T get(Object key) {
        return get(key, null);
    }

    // Get the referenced value - if present.
    public T get(Object key, T dflt) {
        Map.Entry<String, T> e = entries.get((String) key);
        return e != null ? e.getValue() : dflt;
    }

    // Add to the underlying map.
    @Override
    public T put(String key, T value) {
        T old = null;
        // Do I have an entry for it already?
        Map.Entry<String, T> entry = entries.get(key);
        // Was it already there?
        if (entry != null) {
            // Yes. Just update it.
            old = entry.setValue(value);
        } else {
            // Add it to the map.
            map.put(prefix + key, value);
            // Rebuild.
            rebuildEntries();
        }
        return old;
    }

    // Get rid of that one.
    @Override
    public T remove(Object key) {
        // Do I have an entry for it?
        Map.Entry<String, T> entry = entries.get((String) key);
        if (entry != null) {
            entries.remove(key);
            // Change the underlying map.
            return map.remove(prefix + key);
        }
        return null;
    }

    // Add all of them.
    @Override
    public void putAll(Map<? extends String, ? extends T> m) {
        for (Map.Entry<? extends String, ? extends T> e : m.entrySet()) {
            put(e.getKey(), e.getValue());
        }
    }

    // Clear everything out.
    @Override
    public void clear() {
        // Just remove mine.
        // This does not clear the underlying map - perhaps it should remove the filtered entries.
        for (String key : entries.keySet()) {
            map.remove(prefix + key);
        }
        entries.clear();
    }

    @Override
    public Set<String> keySet() {
        return entries.keySet();
    }

    @Override
    public Collection<T> values() {
        // Roll them all out into a new ArrayList.
        List<T> values = new ArrayList<>();
        for (Map.Entry<String, T> v : entries.values()) {
            values.add(v.getValue());
        }
        return values;
    }

    @Override
    public Set<Map.Entry<String, T>> entrySet() {
        // Roll them all out into a new TreeSet.
        Set<Map.Entry<String, T>> entrySet = new TreeSet<>();
        for (Map.Entry<String, Map.Entry<String, T>> v : entries.entrySet()) {
            entrySet.add(new Entry<>(v));
        }
        return entrySet;
    }

    /**
     * An entry.
     *
     * @param <T>
     *
     * The type of the value.
     */
    private static class Entry<T> implements Map.Entry<String, T>, Comparable<Entry<T>> {

        // Note that entry in the entry is an entry in the underlying map.
        private final Map.Entry<String, Map.Entry<String, T>> entry;

        Entry(Map.Entry<String, Map.Entry<String, T>> entry) {
            this.entry = entry;
        }

        @Override
        public String getKey() {
            return entry.getKey();
        }

        @Override
        public T getValue() {
            // Remember that the value is the entry in the underlying map.
            return entry.getValue().getValue();
        }

        @Override
        public T setValue(T newValue) {
            // Remember that the value is the entry in the underlying map.
            return entry.getValue().setValue(newValue);
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof Entry)) {
                return false;
            }
            Entry e = (Entry) o;
            return getKey().equals(e.getKey()) && getValue().equals(e.getValue());
        }

        @Override
        public int hashCode() {
            return getKey().hashCode() ^ getValue().hashCode();
        }

        @Override
        public String toString() {
            return getKey() + "=" + getValue();
        }

        @Override
        public int compareTo(Entry<T> o) {
            return getKey().compareTo(o.getKey());
        }
    }

    // Simple tests.
    public static void main(String[] args) {
        String[] samples = {
            "Some.For.Me",
            "Some.For.You",
            "Some.More",
            "Yet.More"};
        Map map = new HashMap();
        for (String s : samples) {
            map.put(s, s);
        }
        Map all = new MapFilter(map);
        Map some = new MapFilter(map, "Some.");
        Map someFor = new MapFilter(some, "For.");
        System.out.println("All: " + all);
        System.out.println("Some: " + some);
        System.out.println("Some.For: " + someFor);
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionHansjoerg WingeierView Question on Stackoverflow
Solution 1 - JavaAndy WilkinsonView Answer on Stackoverflow
Solution 2 - JavaMMascarinView Answer on Stackoverflow
Solution 3 - JavaasherbretView Answer on Stackoverflow
Solution 4 - JavaOldCurmudgeonView Answer on Stackoverflow