-
Couldn't load subscription status.
- Fork 77
Add support for count projections and KeySpaceStore abstraction
#654
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| /* | ||
| * Copyright 2025 the original author or authors. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package org.springframework.data.map; | ||
|
|
||
| import java.util.Map; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
|
|
||
| /** | ||
| * Strategy interface to obtain a map for a given key space. Implementations should be thread-safe when intended for use | ||
| * with multiple threads (both, the store itself and the used keystore maps). | ||
| * <p> | ||
| * Can be used to plug in keystore creation or implementation strategies (for example, Map-based implementations such as | ||
| * MapDB or Infinispan) through a consolidated interface. A keyspace store represents a map of maps or a database with | ||
| * multiple collections and can use any kind of map per keyspace. | ||
| * <p> | ||
| * For example, a {@link ConcurrentHashMap} can be used as keystore map type to allow concurrent access to keyspaces | ||
| * using: | ||
| * | ||
| * <pre class="code"> | ||
| * KeyspaceStore store = KeyspaceStore.create(); | ||
| * </pre> | ||
| * | ||
| * Custom map types (or instances of these) can be used as well using the provided factory methods: | ||
| * | ||
| * <pre class="code"> | ||
| * KeyspaceStore store = KeyspaceStore.of(LinkedHashMap.class); | ||
| * | ||
| * Map<String, Map<Object, Object>> backingMap = …; | ||
| * KeyspaceStore store = KeyspaceStore.of(backingMap); | ||
| * </pre> | ||
| * | ||
| * @since 4.0 | ||
| */ | ||
| public interface KeySpaceStore { | ||
|
|
||
| /** | ||
| * Return the map associated with given keyspace. Implementations can return an empty map if the keyspace does not | ||
| * exist yet or a reference to the map that represents an existing keyspace holding keys and values for the requested | ||
| * keyspace. | ||
| * | ||
| * @param keyspace name of the keyspace to obtain the map for, must not be {@literal null}. | ||
| * @return the map associated with the given keyspace, never {@literal null}. | ||
| */ | ||
| Map<Object, Object> getKeySpace(String keyspace); | ||
|
|
||
| /** | ||
| * Clear all keyspaces. Access to {@link #getKeySpace(String)} will return an empty map for each keyspace after this | ||
| * method call. It is not required to clear each keyspace individually but it makes sense to do so to free up memory. | ||
| */ | ||
| void clear(); | ||
|
|
||
| /** | ||
| * Create a new {@link KeySpaceStore} using {@link ConcurrentHashMap} as backing map type for each keyspace map. | ||
| * | ||
| * @return a new and empty {@link KeySpaceStore}. | ||
| */ | ||
| static KeySpaceStore create() { | ||
| return MapKeySpaceStore.create(); | ||
| } | ||
|
|
||
| /** | ||
| * Create new {@link KeySpaceStore} using given map type for each keyspace map. | ||
| * | ||
| * @param mapType map type to use. | ||
| * @return the new {@link KeySpaceStore} object. | ||
| */ | ||
| @SuppressWarnings("rawtypes") | ||
| static KeySpaceStore of(Class<? extends Map> mapType) { | ||
| return MapKeySpaceStore.of(mapType); | ||
| } | ||
|
|
||
| /** | ||
| * Create new {@link KeySpaceStore} using given map as backing store. Determines the map type from the given map. | ||
| * | ||
| * @param store map of maps. | ||
| * @return the new {@link KeySpaceStore} object for the given {@code store}. | ||
| */ | ||
| static KeySpaceStore of(Map<String, Map<Object, Object>> store) { | ||
| return MapKeySpaceStore.of(store); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| /* | ||
| * Copyright 2025 the original author or authors. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package org.springframework.data.map; | ||
|
|
||
| import java.util.Map; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
|
|
||
| import org.springframework.core.CollectionFactory; | ||
| import org.springframework.util.Assert; | ||
| import org.springframework.util.ClassUtils; | ||
|
|
||
| /** | ||
| * Keyspace store that uses a map of maps to store keyspace data. The outer map holds the keyspaces and the inner maps | ||
| * hold the actual keys and values. | ||
| * | ||
| * @param store reference to the map of maps holding the keyspace data. | ||
| * @param keySpaceMapType map type to be used for each keyspace map. | ||
| * @param initialCapacity initial keyspace map capacity to optimize allocations. | ||
| * @since 4.0 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Author? |
||
| */ | ||
| @SuppressWarnings("rawtypes") | ||
| record MapKeySpaceStore(Map<String, Map<Object, Object>> store, Class<? extends Map> keySpaceMapType, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting use of record as the impl for the store. Any limitations of using record for this? Will the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using records as interface implementations is a neat pattern as our API surface is limited by the interface. Clearly, |
||
| int initialCapacity) implements KeySpaceStore { | ||
|
|
||
| public static final int DEFAULT_INITIAL_CAPACITY = 1000; | ||
|
|
||
| /** | ||
| * Create a new {@link KeySpaceStore} using {@link ConcurrentHashMap} as backing map type for each keyspace map. | ||
| * | ||
| * @return a new and empty {@link KeySpaceStore}. | ||
| */ | ||
| public static KeySpaceStore create() { | ||
| return new MapKeySpaceStore(new ConcurrentHashMap<>(100), ConcurrentHashMap.class, DEFAULT_INITIAL_CAPACITY); | ||
| } | ||
|
|
||
| /** | ||
| * Create new {@link KeySpaceStore} using given map type for each keyspace map. | ||
| * | ||
| * @param mapType map type to use. | ||
| * @return the new {@link KeySpaceStore} object. | ||
| */ | ||
| public static KeySpaceStore of(Class<? extends Map> mapType) { | ||
|
|
||
| Assert.notNull(mapType, "Store map type must not be null"); | ||
|
|
||
| return of(CollectionFactory.createMap(mapType, 100)); | ||
| } | ||
|
|
||
| /** | ||
| * Create new {@link KeySpaceStore} using given map as backing store. Determines the map type from the given map. | ||
| * | ||
| * @param store map of maps. | ||
| * @return the new {@link KeySpaceStore} object for the given {@code store}. | ||
| */ | ||
| @SuppressWarnings("unchecked") | ||
| public static KeySpaceStore of(Map<String, Map<Object, Object>> store) { | ||
|
|
||
| Assert.notNull(store, "Store map must not be null"); | ||
|
|
||
| Class<? extends Map<?, ?>> userClass = (Class<? extends Map<?, ?>>) ClassUtils.getUserClass(store); | ||
| return new MapKeySpaceStore(store, userClass, DEFAULT_INITIAL_CAPACITY); | ||
| } | ||
|
|
||
| @Override | ||
| public Map<Object, Object> getKeySpace(String keyspace) { | ||
| return store.computeIfAbsent(keyspace, k -> CollectionFactory.createMap(keySpaceMapType, initialCapacity)); | ||
| } | ||
|
|
||
| @Override | ||
| public void clear() { | ||
|
|
||
| store.values().forEach(Map::clear); | ||
| store.clear(); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,15 +22,15 @@ | |
| import java.util.concurrent.ConcurrentHashMap; | ||
|
|
||
| import org.jspecify.annotations.Nullable; | ||
| import org.springframework.core.CollectionFactory; | ||
|
|
||
| import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter; | ||
| import org.springframework.data.keyvalue.core.ForwardingCloseableIterator; | ||
| import org.springframework.data.keyvalue.core.KeyValueAdapter; | ||
| import org.springframework.data.keyvalue.core.PredicateQueryEngine; | ||
| import org.springframework.data.keyvalue.core.QueryEngine; | ||
| import org.springframework.data.keyvalue.core.SortAccessor; | ||
| import org.springframework.data.util.CloseableIterator; | ||
| import org.springframework.util.Assert; | ||
| import org.springframework.util.ClassUtils; | ||
|
|
||
| /** | ||
| * {@link KeyValueAdapter} implementation for {@link Map}. | ||
|
|
@@ -41,15 +41,13 @@ | |
| */ | ||
| public class MapKeyValueAdapter extends AbstractKeyValueAdapter { | ||
|
|
||
| @SuppressWarnings("rawtypes") // | ||
| private final Class<? extends Map> keySpaceMapType; | ||
| private final Map<String, Map<Object, Object>> store; | ||
| private final KeySpaceStore store; | ||
|
|
||
| /** | ||
| * Create new {@link MapKeyValueAdapter} using {@link ConcurrentHashMap} as backing store type. | ||
| */ | ||
| public MapKeyValueAdapter() { | ||
| this(ConcurrentHashMap.class); | ||
| this(MapKeySpaceStore.create()); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason to not use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Initially, a left-over from evolving the interface while moving things around. It lead to some more thoughts on the design and I refined |
||
| } | ||
|
|
||
| /** | ||
|
|
@@ -59,7 +57,7 @@ public MapKeyValueAdapter() { | |
| * @since 2.4 | ||
| */ | ||
| public MapKeyValueAdapter(QueryEngine<? extends KeyValueAdapter, ?, ?> engine) { | ||
| this(ConcurrentHashMap.class, engine); | ||
| this(MapKeySpaceStore.create(), engine); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -69,7 +67,7 @@ public MapKeyValueAdapter(QueryEngine<? extends KeyValueAdapter, ?, ?> engine) { | |
| */ | ||
| @SuppressWarnings("rawtypes") | ||
| public MapKeyValueAdapter(Class<? extends Map> mapType) { | ||
| this(CollectionFactory.createMap(mapType, 100), mapType, null); | ||
| this(MapKeySpaceStore.of(mapType)); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -79,14 +77,9 @@ public MapKeyValueAdapter(Class<? extends Map> mapType) { | |
| * @param sortAccessor accessor granting access to sorting implementation | ||
| * @since 3.1.10 | ||
| */ | ||
| @SuppressWarnings("rawtypes") | ||
| public MapKeyValueAdapter(Class<? extends Map> mapType, SortAccessor<Comparator<?>> sortAccessor) { | ||
|
|
||
| super(sortAccessor); | ||
|
|
||
| Assert.notNull(mapType, "Store must not be null"); | ||
|
|
||
| this.store = CollectionFactory.createMap(mapType, 100); | ||
| this.keySpaceMapType = (Class<? extends Map>) ClassUtils.getUserClass(store); | ||
| this(MapKeySpaceStore.of(mapType), new PredicateQueryEngine(sortAccessor)); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -98,17 +91,16 @@ public MapKeyValueAdapter(Class<? extends Map> mapType, SortAccessor<Comparator< | |
| */ | ||
| @SuppressWarnings("rawtypes") | ||
| public MapKeyValueAdapter(Class<? extends Map> mapType, QueryEngine<? extends KeyValueAdapter, ?, ?> engine) { | ||
| this(CollectionFactory.createMap(mapType, 100), mapType, engine); | ||
| this(MapKeySpaceStore.of(mapType), engine); | ||
| } | ||
|
|
||
| /** | ||
| * Create new instance of {@link MapKeyValueAdapter} using given dataStore for persistence. | ||
| * | ||
| * @param store must not be {@literal null}. | ||
| */ | ||
| @SuppressWarnings({ "rawtypes", "unchecked" }) | ||
| public MapKeyValueAdapter(Map<String, Map<Object, Object>> store) { | ||
| this(store, (Class<? extends Map>) ClassUtils.getUserClass(store), null); | ||
| this(MapKeySpaceStore.of(store)); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -118,29 +110,32 @@ public MapKeyValueAdapter(Map<String, Map<Object, Object>> store) { | |
| * @param engine the query engine. | ||
| * @since 2.4 | ||
| */ | ||
| @SuppressWarnings({ "rawtypes", "unchecked" }) | ||
| public MapKeyValueAdapter(Map<String, Map<Object, Object>> store, QueryEngine<? extends KeyValueAdapter, ?, ?> engine) { | ||
| this(store, (Class<? extends Map>) ClassUtils.getUserClass(store), engine); | ||
| this(MapKeySpaceStore.of(store), engine); | ||
| } | ||
|
|
||
| /** | ||
| * Create new instance of {@link MapKeyValueAdapter} using given dataStore for persistence. | ||
| * | ||
| * @param store must not be {@literal null}. | ||
| */ | ||
| public MapKeyValueAdapter(KeySpaceStore store) { | ||
| this(store, new PredicateQueryEngine()); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a new {@link MapKeyValueAdapter} with the given store and type to be used when creating key spaces and | ||
| * query engine. | ||
| * | ||
| * @param store must not be {@literal null}. | ||
| * @param keySpaceMapType must not be {@literal null}. | ||
| * @param engine the query engine. | ||
| */ | ||
| @SuppressWarnings("rawtypes") | ||
| private MapKeyValueAdapter(Map<String, Map<Object, Object>> store, Class<? extends Map> keySpaceMapType, @Nullable QueryEngine<? extends KeyValueAdapter, ?, ?> engine) { | ||
| public MapKeyValueAdapter(KeySpaceStore store, @Nullable QueryEngine<? extends KeyValueAdapter, ?, ?> engine) { | ||
|
|
||
| super(engine); | ||
|
|
||
| Assert.notNull(store, "Store must not be null"); | ||
| Assert.notNull(keySpaceMapType, "Map type to be used for key spaces must not be null"); | ||
|
|
||
| Assert.notNull(store, "KeyspaceStore must not be null"); | ||
| this.store = store; | ||
| this.keySpaceMapType = keySpaceMapType; | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -210,7 +205,7 @@ public void destroy() throws Exception { | |
| protected Map<Object, Object> getKeySpaceMap(String keyspace) { | ||
|
|
||
| Assert.notNull(keyspace, "Collection must not be null for lookup"); | ||
| return store.computeIfAbsent(keyspace, k -> CollectionFactory.createMap(keySpaceMapType, 1000)); | ||
| return store.getKeySpace(keyspace); | ||
| } | ||
|
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.