18 changed files with 1092 additions and 358 deletions
@ -0,0 +1,62 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2024 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.mongodb; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
import org.springframework.data.util.Lazy; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
|
||||||
|
/** |
||||||
|
* Default implementation of {@link MongoTransactionOptions} using {@literal mongo:} as {@link #getLabelPrefix() label |
||||||
|
* prefix} creating {@link SimpleMongoTransactionOptions} out of a given argument {@link Map}. Uses |
||||||
|
* {@link SimpleMongoTransactionOptions#KNOWN_KEYS} to validate entries in arguments to resolve and errors on unknown |
||||||
|
* entries. |
||||||
|
* |
||||||
|
* @author Christoph Strobl |
||||||
|
* @since 4.3 |
||||||
|
*/ |
||||||
|
class DefaultMongoTransactionOptionsResolver implements MongoTransactionOptionsResolver { |
||||||
|
|
||||||
|
static final Lazy<MongoTransactionOptionsResolver> INSTANCE = Lazy.of(DefaultMongoTransactionOptionsResolver::new); |
||||||
|
|
||||||
|
private static final String PREFIX = "mongo:"; |
||||||
|
|
||||||
|
private DefaultMongoTransactionOptionsResolver() {} |
||||||
|
|
||||||
|
@Override |
||||||
|
public MongoTransactionOptions convert(Map<String, String> options) { |
||||||
|
|
||||||
|
validateKeys(options.keySet()); |
||||||
|
return SimpleMongoTransactionOptions.of(options); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public String getLabelPrefix() { |
||||||
|
return PREFIX; |
||||||
|
} |
||||||
|
|
||||||
|
private static void validateKeys(Set<String> keys) { |
||||||
|
|
||||||
|
if (!keys.stream().allMatch(SimpleMongoTransactionOptions.KNOWN_KEYS::contains)) { |
||||||
|
|
||||||
|
throw new IllegalArgumentException("Transaction labels contained invalid values. Has to be one of %s" |
||||||
|
.formatted(SimpleMongoTransactionOptions.KNOWN_KEYS)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,205 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2024 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.mongodb; |
||||||
|
|
||||||
|
import java.time.Duration; |
||||||
|
import java.util.concurrent.TimeUnit; |
||||||
|
|
||||||
|
import org.springframework.data.mongodb.core.ReadConcernAware; |
||||||
|
import org.springframework.data.mongodb.core.ReadPreferenceAware; |
||||||
|
import org.springframework.data.mongodb.core.WriteConcernAware; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
|
||||||
|
import com.mongodb.Function; |
||||||
|
import com.mongodb.ReadConcern; |
||||||
|
import com.mongodb.ReadPreference; |
||||||
|
import com.mongodb.TransactionOptions; |
||||||
|
import com.mongodb.WriteConcern; |
||||||
|
|
||||||
|
/** |
||||||
|
* Options to be applied within a specific transaction scope. |
||||||
|
* |
||||||
|
* @author Christoph Strobl |
||||||
|
* @since 4.3 |
||||||
|
*/ |
||||||
|
public interface MongoTransactionOptions |
||||||
|
extends TransactionMetadata, ReadConcernAware, ReadPreferenceAware, WriteConcernAware { |
||||||
|
|
||||||
|
/** |
||||||
|
* Value Object representing empty options enforcing client defaults. Returns {@literal null} for all getter methods. |
||||||
|
*/ |
||||||
|
MongoTransactionOptions NONE = new MongoTransactionOptions() { |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public Duration getMaxCommitTime() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public ReadConcern getReadConcern() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public ReadPreference getReadPreference() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public WriteConcern getWriteConcern() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Merge current options with given ones. Will return first non {@literal null} value from getters whereas the |
||||||
|
* {@literal this} has precedence over the given fallbackOptions. |
||||||
|
* |
||||||
|
* @param fallbackOptions can be {@literal null}. |
||||||
|
* @return new instance of {@link MongoTransactionOptions} or this if {@literal fallbackOptions} is {@literal null} or |
||||||
|
* {@link #NONE}. |
||||||
|
*/ |
||||||
|
default MongoTransactionOptions mergeWith(@Nullable MongoTransactionOptions fallbackOptions) { |
||||||
|
|
||||||
|
if (fallbackOptions == null || MongoTransactionOptions.NONE.equals(fallbackOptions)) { |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
return new MongoTransactionOptions() { |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public Duration getMaxCommitTime() { |
||||||
|
return MongoTransactionOptions.this.hasMaxCommitTime() ? MongoTransactionOptions.this.getMaxCommitTime() |
||||||
|
: fallbackOptions.getMaxCommitTime(); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public ReadConcern getReadConcern() { |
||||||
|
return MongoTransactionOptions.this.hasReadConcern() ? MongoTransactionOptions.this.getReadConcern() |
||||||
|
: fallbackOptions.getReadConcern(); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public ReadPreference getReadPreference() { |
||||||
|
return MongoTransactionOptions.this.hasReadPreference() ? MongoTransactionOptions.this.getReadPreference() |
||||||
|
: fallbackOptions.getReadPreference(); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public WriteConcern getWriteConcern() { |
||||||
|
return MongoTransactionOptions.this.hasWriteConcern() ? MongoTransactionOptions.this.getWriteConcern() |
||||||
|
: fallbackOptions.getWriteConcern(); |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Map the current options using the given mapping {@link Function}. |
||||||
|
* |
||||||
|
* @param mappingFunction |
||||||
|
* @return instance of T. |
||||||
|
* @param <T> |
||||||
|
*/ |
||||||
|
default <T> T as(Function<MongoTransactionOptions, T> mappingFunction) { |
||||||
|
return mappingFunction.apply(this); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return MongoDB driver native {@link TransactionOptions}. |
||||||
|
* @see MongoTransactionOptions#as(Function) |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
default TransactionOptions toDriverOptions() { |
||||||
|
|
||||||
|
return as(it -> { |
||||||
|
|
||||||
|
if (MongoTransactionOptions.NONE.equals(it)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
TransactionOptions.Builder builder = TransactionOptions.builder(); |
||||||
|
if (it.hasMaxCommitTime()) { |
||||||
|
builder.maxCommitTime(it.getMaxCommitTime().toMillis(), TimeUnit.MILLISECONDS); |
||||||
|
} |
||||||
|
if (it.hasReadConcern()) { |
||||||
|
builder.readConcern(it.getReadConcern()); |
||||||
|
} |
||||||
|
if (it.hasReadPreference()) { |
||||||
|
builder.readPreference(it.getReadPreference()); |
||||||
|
} |
||||||
|
if (it.hasWriteConcern()) { |
||||||
|
builder.writeConcern(it.getWriteConcern()); |
||||||
|
} |
||||||
|
return builder.build(); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Factory method to wrap given MongoDB driver native {@link TransactionOptions} into {@link MongoTransactionOptions}. |
||||||
|
* |
||||||
|
* @param options |
||||||
|
* @return {@link MongoTransactionOptions#NONE} if given object is {@literal null}. |
||||||
|
*/ |
||||||
|
static MongoTransactionOptions of(@Nullable TransactionOptions options) { |
||||||
|
|
||||||
|
if (options == null) { |
||||||
|
return NONE; |
||||||
|
} |
||||||
|
|
||||||
|
return new MongoTransactionOptions() { |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public Duration getMaxCommitTime() { |
||||||
|
|
||||||
|
Long millis = options.getMaxCommitTime(TimeUnit.MILLISECONDS); |
||||||
|
return millis != null ? Duration.ofMillis(millis) : null; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public ReadConcern getReadConcern() { |
||||||
|
return options.getReadConcern(); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public ReadPreference getReadPreference() { |
||||||
|
return options.getReadPreference(); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public WriteConcern getWriteConcern() { |
||||||
|
return options.getWriteConcern(); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public TransactionOptions toDriverOptions() { |
||||||
|
return options; |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,114 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2024 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.mongodb; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.transaction.TransactionDefinition; |
||||||
|
import org.springframework.transaction.interceptor.TransactionAttribute; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* A {@link TransactionOptionResolver} reading MongoDB specific {@link MongoTransactionOptions transaction options} from |
||||||
|
* a {@link TransactionDefinition}. Implementations of {@link MongoTransactionOptions} may choose a specific |
||||||
|
* {@link #getLabelPrefix() prefix} for {@link TransactionAttribute#getLabels() transaction attribute labels} to avoid |
||||||
|
* evaluating non store specific ones. |
||||||
|
* <p> |
||||||
|
* {@link TransactionAttribute#getLabels()} evaluated by default should follow the property style using {@code =} to |
||||||
|
* separate key and value pairs. |
||||||
|
* <p> |
||||||
|
* By default {@link #resolve(TransactionDefinition)} will filter labels by the {@link #getLabelPrefix() prefix} and |
||||||
|
* strip the prefix from the label before handing the pruned {@link Map} to the {@link #convert(Map)} function. |
||||||
|
* <p> |
||||||
|
* A transaction definition with labels targeting MongoDB may look like the following: |
||||||
|
* <p> |
||||||
|
* <code class="java"> |
||||||
|
* @Transactional(label = { "mongo:readConcern=majority" }) |
||||||
|
* </code> |
||||||
|
* |
||||||
|
* @author Christoph Strobl |
||||||
|
* @since 4.3 |
||||||
|
*/ |
||||||
|
public interface MongoTransactionOptionsResolver extends TransactionOptionResolver<MongoTransactionOptions> { |
||||||
|
|
||||||
|
/** |
||||||
|
* Obtain the default {@link MongoTransactionOptionsResolver} implementation using a {@literal mongo:} |
||||||
|
* {@link #getLabelPrefix() prefix}. |
||||||
|
* |
||||||
|
* @return instance of default {@link MongoTransactionOptionsResolver} implementation. |
||||||
|
*/ |
||||||
|
static MongoTransactionOptionsResolver defaultResolver() { |
||||||
|
return DefaultMongoTransactionOptionsResolver.INSTANCE.get(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the prefix used to filter applicable {@link TransactionAttribute#getLabels() labels}. |
||||||
|
* |
||||||
|
* @return {@literal null} if no label defined. |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
String getLabelPrefix(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Resolve {@link MongoTransactionOptions} from a given {@link TransactionDefinition} by evaluating |
||||||
|
* {@link TransactionAttribute#getLabels()} labels if possible. |
||||||
|
* <p> |
||||||
|
* Splits applicable labels property style using {@literal =} as deliminator and removes a potential |
||||||
|
* {@link #getLabelPrefix() prefix} before calling {@link #convert(Map)} with filtered label values. |
||||||
|
* |
||||||
|
* @param txDefinition |
||||||
|
* @return {@link MongoTransactionOptions#NONE} in case the given {@link TransactionDefinition} is not a |
||||||
|
* {@link TransactionAttribute} if no matching {@link TransactionAttribute#getLabels() labels} could be found. |
||||||
|
* @throws IllegalArgumentException for options that do not map to valid transactions options or malformatted labels. |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
default MongoTransactionOptions resolve(TransactionDefinition txDefinition) { |
||||||
|
|
||||||
|
if (!(txDefinition instanceof TransactionAttribute attribute)) { |
||||||
|
return MongoTransactionOptions.NONE; |
||||||
|
} |
||||||
|
|
||||||
|
if (attribute.getLabels().isEmpty()) { |
||||||
|
return MongoTransactionOptions.NONE; |
||||||
|
} |
||||||
|
|
||||||
|
Map<String, String> attributeMap = attribute.getLabels().stream() |
||||||
|
.filter(it -> !StringUtils.hasText(getLabelPrefix()) || it.startsWith(getLabelPrefix())) |
||||||
|
.map(it -> StringUtils.hasText(getLabelPrefix()) ? it.substring(getLabelPrefix().length()) : it).map(it -> { |
||||||
|
|
||||||
|
String[] kvPair = StringUtils.split(it, "="); |
||||||
|
Assert.isTrue(kvPair != null && kvPair.length == 2, |
||||||
|
() -> "No value present for transaction option %s".formatted(kvPair != null ? kvPair[0] : it)); |
||||||
|
return kvPair; |
||||||
|
}) |
||||||
|
|
||||||
|
.collect(Collectors.toMap(it -> it[0].trim(), it -> it[1].trim())); |
||||||
|
|
||||||
|
return attributeMap.isEmpty() ? MongoTransactionOptions.NONE : convert(attributeMap); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Convert the given {@link Map} into an instance of {@link MongoTransactionOptions}. |
||||||
|
* |
||||||
|
* @param options never {@literal null}. |
||||||
|
* @return never {@literal null}. |
||||||
|
* @throws IllegalArgumentException for invalid options. |
||||||
|
*/ |
||||||
|
MongoTransactionOptions convert(Map<String, String> options); |
||||||
|
} |
||||||
@ -1,98 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2023 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.mongodb; |
|
||||||
|
|
||||||
import java.time.Duration; |
|
||||||
import java.util.concurrent.TimeUnit; |
|
||||||
|
|
||||||
import org.apache.commons.logging.Log; |
|
||||||
import org.apache.commons.logging.LogFactory; |
|
||||||
import org.springframework.lang.Nullable; |
|
||||||
import org.springframework.transaction.TransactionDefinition; |
|
||||||
import org.springframework.transaction.interceptor.TransactionAttribute; |
|
||||||
|
|
||||||
import com.mongodb.ReadConcern; |
|
||||||
import com.mongodb.ReadConcernLevel; |
|
||||||
import com.mongodb.ReadPreference; |
|
||||||
import com.mongodb.TransactionOptions; |
|
||||||
import com.mongodb.WriteConcern; |
|
||||||
|
|
||||||
/** |
|
||||||
* Helper class for translating @Transactional labels into Mongo-specific {@link TransactionOptions}. |
|
||||||
* |
|
||||||
* @author Yan Kardziyaka |
|
||||||
*/ |
|
||||||
public final class MongoTransactionUtils { |
|
||||||
private static final Log LOGGER = LogFactory.getLog(MongoTransactionUtils.class); |
|
||||||
|
|
||||||
private static final String MAX_COMMIT_TIME = "mongo:maxCommitTime"; |
|
||||||
|
|
||||||
private static final String READ_CONCERN_OPTION = "mongo:readConcern"; |
|
||||||
|
|
||||||
private static final String READ_PREFERENCE_OPTION = "mongo:readPreference"; |
|
||||||
|
|
||||||
private static final String WRITE_CONCERN_OPTION = "mongo:writeConcern"; |
|
||||||
|
|
||||||
private MongoTransactionUtils() {} |
|
||||||
|
|
||||||
@Nullable |
|
||||||
public static TransactionOptions extractOptions(TransactionDefinition transactionDefinition, |
|
||||||
@Nullable TransactionOptions fallbackOptions) { |
|
||||||
if (transactionDefinition instanceof TransactionAttribute transactionAttribute) { |
|
||||||
TransactionOptions.Builder builder = null; |
|
||||||
for (String label : transactionAttribute.getLabels()) { |
|
||||||
String[] tokens = label.split("=", 2); |
|
||||||
builder = tokens.length == 2 ? enhanceWithProperty(builder, tokens[0], tokens[1]) : builder; |
|
||||||
} |
|
||||||
if (builder == null) { |
|
||||||
return fallbackOptions; |
|
||||||
} |
|
||||||
TransactionOptions options = builder.build(); |
|
||||||
return fallbackOptions == null ? options : TransactionOptions.merge(options, fallbackOptions); |
|
||||||
} else { |
|
||||||
if (LOGGER.isDebugEnabled()) { |
|
||||||
LOGGER.debug("%s cannot be casted to %s. Transaction labels won't be evaluated as options".formatted( |
|
||||||
TransactionDefinition.class.getName(), TransactionAttribute.class.getName())); |
|
||||||
} |
|
||||||
return fallbackOptions; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Nullable |
|
||||||
private static TransactionOptions.Builder enhanceWithProperty(@Nullable TransactionOptions.Builder builder, |
|
||||||
String key, String value) { |
|
||||||
return switch (key) { |
|
||||||
case MAX_COMMIT_TIME -> nullSafe(builder).maxCommitTime(Duration.parse(value).toMillis(), TimeUnit.MILLISECONDS); |
|
||||||
case READ_CONCERN_OPTION -> nullSafe(builder).readConcern(new ReadConcern(ReadConcernLevel.fromString(value))); |
|
||||||
case READ_PREFERENCE_OPTION -> nullSafe(builder).readPreference(ReadPreference.valueOf(value)); |
|
||||||
case WRITE_CONCERN_OPTION -> nullSafe(builder).writeConcern(getWriteConcern(value)); |
|
||||||
default -> builder; |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
private static TransactionOptions.Builder nullSafe(@Nullable TransactionOptions.Builder builder) { |
|
||||||
return builder == null ? TransactionOptions.builder() : builder; |
|
||||||
} |
|
||||||
|
|
||||||
private static WriteConcern getWriteConcern(String writeConcernAsString) { |
|
||||||
WriteConcern writeConcern = WriteConcern.valueOf(writeConcernAsString); |
|
||||||
if (writeConcern == null) { |
|
||||||
throw new IllegalArgumentException("'%s' is not a valid WriteConcern".formatted(writeConcernAsString)); |
|
||||||
} |
|
||||||
return writeConcern; |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -0,0 +1,154 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2024 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.mongodb; |
||||||
|
|
||||||
|
import java.time.Duration; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
|
||||||
|
import com.mongodb.Function; |
||||||
|
import com.mongodb.ReadConcern; |
||||||
|
import com.mongodb.ReadConcernLevel; |
||||||
|
import com.mongodb.ReadPreference; |
||||||
|
import com.mongodb.WriteConcern; |
||||||
|
|
||||||
|
/** |
||||||
|
* Trivial implementation of {@link MongoTransactionOptions}. |
||||||
|
* |
||||||
|
* @author Christoph Strobl |
||||||
|
* @since 4.3 |
||||||
|
*/ |
||||||
|
class SimpleMongoTransactionOptions implements MongoTransactionOptions { |
||||||
|
|
||||||
|
static final Set<String> KNOWN_KEYS = Arrays.stream(OptionKey.values()).map(OptionKey::getKey) |
||||||
|
.collect(Collectors.toSet()); |
||||||
|
|
||||||
|
private final Duration maxCommitTime; |
||||||
|
private final ReadConcern readConcern; |
||||||
|
private final ReadPreference readPreference; |
||||||
|
private final WriteConcern writeConcern; |
||||||
|
|
||||||
|
static SimpleMongoTransactionOptions of(Map<String, String> options) { |
||||||
|
return new SimpleMongoTransactionOptions(options); |
||||||
|
} |
||||||
|
|
||||||
|
private SimpleMongoTransactionOptions(Map<String, String> options) { |
||||||
|
|
||||||
|
this.maxCommitTime = doGetMaxCommitTime(options); |
||||||
|
this.readConcern = doGetReadConcern(options); |
||||||
|
this.readPreference = doGetReadPreference(options); |
||||||
|
this.writeConcern = doGetWriteConcern(options); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public Duration getMaxCommitTime() { |
||||||
|
return maxCommitTime; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public ReadConcern getReadConcern() { |
||||||
|
return readConcern; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public ReadPreference getReadPreference() { |
||||||
|
return readPreference; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public WriteConcern getWriteConcern() { |
||||||
|
return writeConcern; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private static Duration doGetMaxCommitTime(Map<String, String> options) { |
||||||
|
|
||||||
|
return getValue(options, OptionKey.MAX_COMMIT_TIME, value -> { |
||||||
|
|
||||||
|
Duration timeout = Duration.parse(value); |
||||||
|
Assert.isTrue(!timeout.isNegative(), "%s cannot be negative".formatted(OptionKey.MAX_COMMIT_TIME)); |
||||||
|
return timeout; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private static ReadConcern doGetReadConcern(Map<String, String> options) { |
||||||
|
return getValue(options, OptionKey.READ_CONCERN, value -> new ReadConcern(ReadConcernLevel.fromString(value))); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private static ReadPreference doGetReadPreference(Map<String, String> options) { |
||||||
|
return getValue(options, OptionKey.READ_PREFERENCE, ReadPreference::valueOf); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private static WriteConcern doGetWriteConcern(Map<String, String> options) { |
||||||
|
|
||||||
|
return getValue(options, OptionKey.WRITE_CONCERN, value -> { |
||||||
|
|
||||||
|
WriteConcern writeConcern = WriteConcern.valueOf(value); |
||||||
|
if (writeConcern == null) { |
||||||
|
throw new IllegalArgumentException("'%s' is not a valid WriteConcern".formatted(options.get("writeConcern"))); |
||||||
|
} |
||||||
|
return writeConcern; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private static <T> T getValue(Map<String, String> options, OptionKey key, Function<String, T> convertFunction) { |
||||||
|
|
||||||
|
String value = options.get(key.getKey()); |
||||||
|
return value != null ? convertFunction.apply(value) : null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
|
||||||
|
return "DefaultMongoTransactionOptions{" + "maxCommitTime=" + maxCommitTime + ", readConcern=" + readConcern |
||||||
|
+ ", readPreference=" + readPreference + ", writeConcern=" + writeConcern + '}'; |
||||||
|
} |
||||||
|
|
||||||
|
enum OptionKey { |
||||||
|
|
||||||
|
MAX_COMMIT_TIME("maxCommitTime"), READ_CONCERN("readConcern"), READ_PREFERENCE("readPreference"), WRITE_CONCERN( |
||||||
|
"writeConcern"); |
||||||
|
|
||||||
|
final String key; |
||||||
|
|
||||||
|
OptionKey(String key) { |
||||||
|
this.key = key; |
||||||
|
} |
||||||
|
|
||||||
|
public String getKey() { |
||||||
|
return key; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
return getKey(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,34 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2024 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.mongodb; |
||||||
|
|
||||||
|
import java.time.Duration; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Christoph Strobl |
||||||
|
* @since 4.3 |
||||||
|
*/ |
||||||
|
public interface TransactionMetadata { |
||||||
|
|
||||||
|
@Nullable |
||||||
|
Duration getMaxCommitTime(); |
||||||
|
|
||||||
|
default boolean hasMaxCommitTime() { |
||||||
|
return getMaxCommitTime() != null; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,29 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2024 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.mongodb; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.transaction.TransactionDefinition; |
||||||
|
import org.springframework.transaction.interceptor.TransactionAttribute; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Christoph Strobl |
||||||
|
*/ |
||||||
|
interface TransactionOptionResolver<T extends TransactionMetadata> { |
||||||
|
|
||||||
|
@Nullable |
||||||
|
T resolve(TransactionDefinition attribute); |
||||||
|
} |
||||||
@ -0,0 +1,41 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2024 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.mongodb.core; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
|
||||||
|
import com.mongodb.ReadPreference; |
||||||
|
import com.mongodb.WriteConcern; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Christoph Strobl |
||||||
|
* @since 4.3 |
||||||
|
*/ |
||||||
|
public interface WriteConcernAware { |
||||||
|
|
||||||
|
/** |
||||||
|
* @return {@literal true} if a {@link com.mongodb.WriteConcern} is set. |
||||||
|
*/ |
||||||
|
default boolean hasWriteConcern() { |
||||||
|
return getWriteConcern() != null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return the {@link ReadPreference} to apply or {@literal null} if none set. |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
WriteConcern getWriteConcern(); |
||||||
|
} |
||||||
@ -0,0 +1,64 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2024 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.mongodb; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions; |
||||||
|
import org.assertj.core.api.ListAssert; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.CollectionUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Christoph Strobl |
||||||
|
*/ |
||||||
|
public class CapturingTransactionOptionsResolver implements MongoTransactionOptionsResolver { |
||||||
|
|
||||||
|
private final MongoTransactionOptionsResolver delegateResolver; |
||||||
|
private final List<MongoTransactionOptions> capturedOptions = new ArrayList<>(10); |
||||||
|
|
||||||
|
public CapturingTransactionOptionsResolver(MongoTransactionOptionsResolver delegateResolver) { |
||||||
|
this.delegateResolver = delegateResolver; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public String getLabelPrefix() { |
||||||
|
return delegateResolver.getLabelPrefix(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public MongoTransactionOptions convert(Map<String, String> source) { |
||||||
|
|
||||||
|
MongoTransactionOptions options = delegateResolver.convert(source); |
||||||
|
capturedOptions.add(options); |
||||||
|
return options; |
||||||
|
} |
||||||
|
|
||||||
|
public void clear() { |
||||||
|
capturedOptions.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
public List<MongoTransactionOptions> getCapturedOptions() { |
||||||
|
return capturedOptions; |
||||||
|
} |
||||||
|
|
||||||
|
public MongoTransactionOptions getLastCapturedOption() { |
||||||
|
return CollectionUtils.lastElement(capturedOptions); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,116 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2024 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.mongodb; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.*; |
||||||
|
|
||||||
|
import java.time.Duration; |
||||||
|
import java.util.concurrent.TimeUnit; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
|
||||||
|
import com.mongodb.ReadConcern; |
||||||
|
import com.mongodb.ReadPreference; |
||||||
|
import com.mongodb.TransactionOptions; |
||||||
|
import com.mongodb.WriteConcern; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Christoph Strobl |
||||||
|
*/ |
||||||
|
class MongoTransactionOptionsUnitTests { |
||||||
|
|
||||||
|
private static final TransactionOptions NATIVE_OPTIONS = TransactionOptions.builder() //
|
||||||
|
.maxCommitTime(1L, TimeUnit.SECONDS) //
|
||||||
|
.readConcern(ReadConcern.SNAPSHOT) //
|
||||||
|
.readPreference(ReadPreference.secondaryPreferred()) //
|
||||||
|
.writeConcern(WriteConcern.W3) //
|
||||||
|
.build(); |
||||||
|
|
||||||
|
@Test // GH-1628
|
||||||
|
void wrapsNativeDriverTransactionOptions() { |
||||||
|
|
||||||
|
assertThat(MongoTransactionOptions.of(NATIVE_OPTIONS)) |
||||||
|
.returns(NATIVE_OPTIONS.getMaxCommitTime(TimeUnit.SECONDS), options -> options.getMaxCommitTime().toSeconds()) |
||||||
|
.returns(NATIVE_OPTIONS.getReadConcern(), MongoTransactionOptions::getReadConcern) |
||||||
|
.returns(NATIVE_OPTIONS.getReadPreference(), MongoTransactionOptions::getReadPreference) |
||||||
|
.returns(NATIVE_OPTIONS.getWriteConcern(), MongoTransactionOptions::getWriteConcern) |
||||||
|
.returns(NATIVE_OPTIONS, MongoTransactionOptions::toDriverOptions); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // GH-1628
|
||||||
|
void mergeNoneWithDefaultsUsesDefaults() { |
||||||
|
|
||||||
|
assertThat(MongoTransactionOptions.NONE.mergeWith(MongoTransactionOptions.of(NATIVE_OPTIONS))) |
||||||
|
.returns(NATIVE_OPTIONS.getMaxCommitTime(TimeUnit.SECONDS), options -> options.getMaxCommitTime().toSeconds()) |
||||||
|
.returns(NATIVE_OPTIONS.getReadConcern(), MongoTransactionOptions::getReadConcern) |
||||||
|
.returns(NATIVE_OPTIONS.getReadPreference(), MongoTransactionOptions::getReadPreference) |
||||||
|
.returns(NATIVE_OPTIONS.getWriteConcern(), MongoTransactionOptions::getWriteConcern) |
||||||
|
.returns(NATIVE_OPTIONS, MongoTransactionOptions::toDriverOptions); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // GH-1628
|
||||||
|
void mergeExistingOptionsWithNoneUsesOptions() { |
||||||
|
|
||||||
|
MongoTransactionOptions source = MongoTransactionOptions.of(NATIVE_OPTIONS); |
||||||
|
assertThat(source.mergeWith(MongoTransactionOptions.NONE)).isSameAs(source); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // GH-1628
|
||||||
|
void mergeExistingOptionsWithUsesFirstNonNullValue() { |
||||||
|
|
||||||
|
MongoTransactionOptions source = MongoTransactionOptions |
||||||
|
.of(TransactionOptions.builder().writeConcern(WriteConcern.UNACKNOWLEDGED).build()); |
||||||
|
|
||||||
|
assertThat(source.mergeWith(MongoTransactionOptions.of(NATIVE_OPTIONS))) |
||||||
|
.returns(NATIVE_OPTIONS.getMaxCommitTime(TimeUnit.SECONDS), options -> options.getMaxCommitTime().toSeconds()) |
||||||
|
.returns(NATIVE_OPTIONS.getReadConcern(), MongoTransactionOptions::getReadConcern) |
||||||
|
.returns(NATIVE_OPTIONS.getReadPreference(), MongoTransactionOptions::getReadPreference) |
||||||
|
.returns(source.getWriteConcern(), MongoTransactionOptions::getWriteConcern); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // GH-1628
|
||||||
|
void testEquals() { |
||||||
|
|
||||||
|
assertThat(MongoTransactionOptions.NONE) //
|
||||||
|
.isSameAs(MongoTransactionOptions.NONE) //
|
||||||
|
.isNotEqualTo(new MongoTransactionOptions() { |
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public Duration getMaxCommitTime() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public ReadConcern getReadConcern() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public ReadPreference getReadPreference() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public WriteConcern getWriteConcern() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
@ -1,227 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2023 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.mongodb; |
|
||||||
|
|
||||||
import static java.util.UUID.*; |
|
||||||
import static org.assertj.core.api.Assertions.*; |
|
||||||
|
|
||||||
import java.util.Set; |
|
||||||
import java.util.concurrent.TimeUnit; |
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test; |
|
||||||
import org.springframework.transaction.interceptor.DefaultTransactionAttribute; |
|
||||||
import org.springframework.transaction.interceptor.TransactionAttribute; |
|
||||||
import org.springframework.transaction.support.DefaultTransactionDefinition; |
|
||||||
|
|
||||||
import com.mongodb.ReadConcern; |
|
||||||
import com.mongodb.ReadPreference; |
|
||||||
import com.mongodb.TransactionOptions; |
|
||||||
import com.mongodb.WriteConcern; |
|
||||||
|
|
||||||
/** |
|
||||||
* @author Yan Kardziyaka |
|
||||||
*/ |
|
||||||
class MongoTransactionUtilsUnitTests { |
|
||||||
|
|
||||||
@Test // GH-1628
|
|
||||||
public void shouldThrowIllegalArgumentExceptionIfLabelsContainInvalidMaxCommitTime() { |
|
||||||
TransactionOptions fallbackOptions = getTransactionOptions(); |
|
||||||
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); |
|
||||||
attribute.setLabels(Set.of("mongo:maxCommitTime=-PT5S")); |
|
||||||
|
|
||||||
assertThatThrownBy(() -> MongoTransactionUtils.extractOptions(attribute, fallbackOptions)) //
|
|
||||||
.isInstanceOf(IllegalArgumentException.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Test // GH-1628
|
|
||||||
public void shouldThrowIllegalArgumentExceptionIfLabelsContainInvalidReadConcern() { |
|
||||||
TransactionOptions fallbackOptions = getTransactionOptions(); |
|
||||||
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); |
|
||||||
attribute.setLabels(Set.of("mongo:readConcern=invalidValue")); |
|
||||||
|
|
||||||
assertThatThrownBy(() -> MongoTransactionUtils.extractOptions(attribute, fallbackOptions)) //
|
|
||||||
.isInstanceOf(IllegalArgumentException.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Test // GH-1628
|
|
||||||
public void shouldThrowIllegalArgumentExceptionIfLabelsContainInvalidReadPreference() { |
|
||||||
TransactionOptions fallbackOptions = getTransactionOptions(); |
|
||||||
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); |
|
||||||
attribute.setLabels(Set.of("mongo:readPreference=invalidValue")); |
|
||||||
|
|
||||||
assertThatThrownBy(() -> MongoTransactionUtils.extractOptions(attribute, fallbackOptions)) //
|
|
||||||
.isInstanceOf(IllegalArgumentException.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Test // GH-1628
|
|
||||||
public void shouldThrowIllegalArgumentExceptionIfLabelsContainInvalidWriteConcern() { |
|
||||||
TransactionOptions fallbackOptions = getTransactionOptions(); |
|
||||||
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); |
|
||||||
attribute.setLabels(Set.of("mongo:writeConcern=invalidValue")); |
|
||||||
|
|
||||||
assertThatThrownBy(() -> MongoTransactionUtils.extractOptions(attribute, fallbackOptions)) //
|
|
||||||
.isInstanceOf(IllegalArgumentException.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Test // GH-1628
|
|
||||||
public void shouldReturnFallbackOptionsIfNotTransactionAttribute() { |
|
||||||
TransactionOptions fallbackOptions = getTransactionOptions(); |
|
||||||
DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); |
|
||||||
|
|
||||||
TransactionOptions result = MongoTransactionUtils.extractOptions(definition, fallbackOptions); |
|
||||||
|
|
||||||
assertThat(result).isSameAs(fallbackOptions); |
|
||||||
} |
|
||||||
|
|
||||||
@Test // GH-1628
|
|
||||||
public void shouldReturnFallbackOptionsIfNoLabelsProvided() { |
|
||||||
TransactionOptions fallbackOptions = getTransactionOptions(); |
|
||||||
TransactionAttribute attribute = new DefaultTransactionAttribute(); |
|
||||||
|
|
||||||
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, fallbackOptions); |
|
||||||
|
|
||||||
assertThat(result).isSameAs(fallbackOptions); |
|
||||||
} |
|
||||||
|
|
||||||
@Test // GH-1628
|
|
||||||
public void shouldReturnFallbackOptionsIfLabelsDoesNotContainValidOptions() { |
|
||||||
TransactionOptions fallbackOptions = getTransactionOptions(); |
|
||||||
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); |
|
||||||
Set<String> labels = Set.of("mongo:readConcern", "writeConcern", "readPreference=SECONDARY", |
|
||||||
"mongo:maxCommitTime PT5M", randomUUID().toString()); |
|
||||||
attribute.setLabels(labels); |
|
||||||
|
|
||||||
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, fallbackOptions); |
|
||||||
|
|
||||||
assertThat(result).isSameAs(fallbackOptions); |
|
||||||
} |
|
||||||
|
|
||||||
@Test // GH-1628
|
|
||||||
public void shouldReturnMergedOptionsIfLabelsContainMaxCommitTime() { |
|
||||||
TransactionOptions fallbackOptions = getTransactionOptions(); |
|
||||||
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); |
|
||||||
attribute.setLabels(Set.of("mongo:maxCommitTime=PT5S")); |
|
||||||
|
|
||||||
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, fallbackOptions); |
|
||||||
|
|
||||||
assertThat(result).isNotSameAs(fallbackOptions) //
|
|
||||||
.returns(5L, from(options -> options.getMaxCommitTime(TimeUnit.SECONDS))) //
|
|
||||||
.returns(ReadConcern.AVAILABLE, from(TransactionOptions::getReadConcern)) //
|
|
||||||
.returns(ReadPreference.secondaryPreferred(), from(TransactionOptions::getReadPreference)) //
|
|
||||||
.returns(WriteConcern.UNACKNOWLEDGED, from(TransactionOptions::getWriteConcern)); |
|
||||||
} |
|
||||||
|
|
||||||
@Test // GH-1628
|
|
||||||
public void shouldReturnMergedOptionsIfLabelsContainReadConcern() { |
|
||||||
TransactionOptions fallbackOptions = getTransactionOptions(); |
|
||||||
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); |
|
||||||
attribute.setLabels(Set.of("mongo:readConcern=majority")); |
|
||||||
|
|
||||||
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, fallbackOptions); |
|
||||||
|
|
||||||
assertThat(result).isNotSameAs(fallbackOptions) //
|
|
||||||
.returns(1L, from(options -> options.getMaxCommitTime(TimeUnit.MINUTES))) //
|
|
||||||
.returns(ReadConcern.MAJORITY, from(TransactionOptions::getReadConcern)) //
|
|
||||||
.returns(ReadPreference.secondaryPreferred(), from(TransactionOptions::getReadPreference)) //
|
|
||||||
.returns(WriteConcern.UNACKNOWLEDGED, from(TransactionOptions::getWriteConcern)); |
|
||||||
} |
|
||||||
|
|
||||||
@Test // GH-1628
|
|
||||||
public void shouldReturnMergedOptionsIfLabelsContainReadPreference() { |
|
||||||
TransactionOptions fallbackOptions = getTransactionOptions(); |
|
||||||
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); |
|
||||||
attribute.setLabels(Set.of("mongo:readPreference=primaryPreferred")); |
|
||||||
|
|
||||||
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, fallbackOptions); |
|
||||||
|
|
||||||
assertThat(result).isNotSameAs(fallbackOptions) //
|
|
||||||
.returns(1L, from(options -> options.getMaxCommitTime(TimeUnit.MINUTES))) //
|
|
||||||
.returns(ReadConcern.AVAILABLE, from(TransactionOptions::getReadConcern)) //
|
|
||||||
.returns(ReadPreference.primaryPreferred(), from(TransactionOptions::getReadPreference)) //
|
|
||||||
.returns(WriteConcern.UNACKNOWLEDGED, from(TransactionOptions::getWriteConcern)); |
|
||||||
} |
|
||||||
|
|
||||||
@Test // GH-1628
|
|
||||||
public void shouldReturnMergedOptionsIfLabelsContainWriteConcern() { |
|
||||||
TransactionOptions fallbackOptions = getTransactionOptions(); |
|
||||||
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); |
|
||||||
attribute.setLabels(Set.of("mongo:writeConcern=w3")); |
|
||||||
|
|
||||||
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, fallbackOptions); |
|
||||||
|
|
||||||
assertThat(result).isNotSameAs(fallbackOptions) //
|
|
||||||
.returns(1L, from(options -> options.getMaxCommitTime(TimeUnit.MINUTES))) //
|
|
||||||
.returns(ReadConcern.AVAILABLE, from(TransactionOptions::getReadConcern)) //
|
|
||||||
.returns(ReadPreference.secondaryPreferred(), from(TransactionOptions::getReadPreference)) //
|
|
||||||
.returns(WriteConcern.W3, from(TransactionOptions::getWriteConcern)); |
|
||||||
} |
|
||||||
|
|
||||||
@Test // GH-1628
|
|
||||||
public void shouldReturnNewOptionsIfLabelsContainAllOptions() { |
|
||||||
TransactionOptions fallbackOptions = getTransactionOptions(); |
|
||||||
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); |
|
||||||
Set<String> labels = Set.of("mongo:maxCommitTime=PT5S", "mongo:readConcern=majority", |
|
||||||
"mongo:readPreference=primaryPreferred", "mongo:writeConcern=w3"); |
|
||||||
attribute.setLabels(labels); |
|
||||||
|
|
||||||
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, fallbackOptions); |
|
||||||
|
|
||||||
assertThat(result).isNotSameAs(fallbackOptions) //
|
|
||||||
.returns(5L, from(options -> options.getMaxCommitTime(TimeUnit.SECONDS))) //
|
|
||||||
.returns(ReadConcern.MAJORITY, from(TransactionOptions::getReadConcern)) //
|
|
||||||
.returns(ReadPreference.primaryPreferred(), from(TransactionOptions::getReadPreference)) //
|
|
||||||
.returns(WriteConcern.W3, from(TransactionOptions::getWriteConcern)); |
|
||||||
} |
|
||||||
|
|
||||||
@Test // GH-1628
|
|
||||||
public void shouldReturnMergedOptionsIfLabelsContainOptionsMixedWithOrdinaryStrings() { |
|
||||||
TransactionOptions fallbackOptions = getTransactionOptions(); |
|
||||||
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); |
|
||||||
Set<String> labels = Set.of("mongo:maxCommitTime=PT5S", "mongo:nonExistentOption=value", "label", |
|
||||||
"mongo:writeConcern=w3"); |
|
||||||
attribute.setLabels(labels); |
|
||||||
|
|
||||||
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, fallbackOptions); |
|
||||||
|
|
||||||
assertThat(result).isNotSameAs(fallbackOptions) //
|
|
||||||
.returns(5L, from(options -> options.getMaxCommitTime(TimeUnit.SECONDS))) //
|
|
||||||
.returns(ReadConcern.AVAILABLE, from(TransactionOptions::getReadConcern)) //
|
|
||||||
.returns(ReadPreference.secondaryPreferred(), from(TransactionOptions::getReadPreference)) //
|
|
||||||
.returns(WriteConcern.W3, from(TransactionOptions::getWriteConcern)); |
|
||||||
} |
|
||||||
|
|
||||||
@Test // GH-1628
|
|
||||||
public void shouldReturnNewOptionsIFallbackIsNull() { |
|
||||||
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); |
|
||||||
Set<String> labels = Set.of("mongo:maxCommitTime=PT5S", "mongo:writeConcern=w3"); |
|
||||||
attribute.setLabels(labels); |
|
||||||
|
|
||||||
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, null); |
|
||||||
|
|
||||||
assertThat(result).returns(5L, from(options -> options.getMaxCommitTime(TimeUnit.SECONDS))) //
|
|
||||||
.returns(null, from(TransactionOptions::getReadConcern)) //
|
|
||||||
.returns(null, from(TransactionOptions::getReadPreference)) //
|
|
||||||
.returns(WriteConcern.W3, from(TransactionOptions::getWriteConcern)); |
|
||||||
} |
|
||||||
|
|
||||||
private TransactionOptions getTransactionOptions() { |
|
||||||
return TransactionOptions.builder() //
|
|
||||||
.maxCommitTime(1L, TimeUnit.MINUTES) //
|
|
||||||
.readConcern(ReadConcern.AVAILABLE) //
|
|
||||||
.readPreference(ReadPreference.secondaryPreferred()) //
|
|
||||||
.writeConcern(WriteConcern.UNACKNOWLEDGED).build(); |
|
||||||
} |
|
||||||
} |
|
||||||
@ -0,0 +1,131 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2023 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.mongodb; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.*; |
||||||
|
|
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.junit.jupiter.params.ParameterizedTest; |
||||||
|
import org.junit.jupiter.params.provider.ValueSource; |
||||||
|
import org.springframework.transaction.interceptor.DefaultTransactionAttribute; |
||||||
|
import org.springframework.transaction.interceptor.TransactionAttribute; |
||||||
|
import org.springframework.transaction.support.DefaultTransactionDefinition; |
||||||
|
|
||||||
|
import com.mongodb.ReadConcern; |
||||||
|
import com.mongodb.ReadPreference; |
||||||
|
import com.mongodb.WriteConcern; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Yan Kardziyaka |
||||||
|
* @author Christoph Strobl |
||||||
|
*/ |
||||||
|
class SimpleMongoTransactionOptionsResolverUnitTests { |
||||||
|
|
||||||
|
@ParameterizedTest |
||||||
|
@ValueSource(strings = { "mongo:maxCommitTime=-PT5S", "mongo:readConcern=invalidValue", |
||||||
|
"mongo:readPreference=invalidValue", "mongo:writeConcern=invalidValue", "mongo:invalidPreference=jedi", "mongo:readConcern", "mongo:readConcern:local", "mongo:readConcern=" }) |
||||||
|
void shouldThrowExceptionOnInvalidAttribute(String label) { |
||||||
|
|
||||||
|
TransactionAttribute attribute = transactionAttribute(label); |
||||||
|
|
||||||
|
assertThatThrownBy(() -> DefaultMongoTransactionOptionsResolver.INSTANCE.get().resolve(attribute)) //
|
||||||
|
.isInstanceOf(IllegalArgumentException.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // GH-1628
|
||||||
|
public void shouldReturnEmptyOptionsIfNotTransactionAttribute() { |
||||||
|
|
||||||
|
DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); |
||||||
|
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.get().resolve(definition)) |
||||||
|
.isSameAs(MongoTransactionOptions.NONE); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // GH-1628
|
||||||
|
public void shouldReturnEmptyOptionsIfNoLabelsProvided() { |
||||||
|
|
||||||
|
TransactionAttribute attribute = new DefaultTransactionAttribute(); |
||||||
|
|
||||||
|
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.get().resolve(attribute)) |
||||||
|
.isSameAs(MongoTransactionOptions.NONE); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // GH-1628
|
||||||
|
public void shouldIgnoreNonMongoOptions() { |
||||||
|
|
||||||
|
TransactionAttribute attribute = transactionAttribute("jpa:ignore"); |
||||||
|
|
||||||
|
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.get().resolve(attribute)) |
||||||
|
.isSameAs(MongoTransactionOptions.NONE); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // GH-1628
|
||||||
|
public void shouldReturnMergedOptionsIfLabelsContainMaxCommitTime() { |
||||||
|
|
||||||
|
TransactionAttribute attribute = transactionAttribute("mongo:maxCommitTime=PT5S"); |
||||||
|
|
||||||
|
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.get().resolve(attribute)) |
||||||
|
.returns(5L, from(options -> options.getMaxCommitTime().toSeconds())) //
|
||||||
|
.returns(null, from(MongoTransactionOptions::getReadConcern)) //
|
||||||
|
.returns(null, from(MongoTransactionOptions::getReadPreference)) //
|
||||||
|
.returns(null, from(MongoTransactionOptions::getWriteConcern)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // GH-1628
|
||||||
|
public void shouldReturnReadConcernWhenPresent() { |
||||||
|
|
||||||
|
TransactionAttribute attribute = transactionAttribute("mongo:readConcern=majority"); |
||||||
|
|
||||||
|
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.get().resolve(attribute)) |
||||||
|
.returns(null, from(TransactionMetadata::getMaxCommitTime)) //
|
||||||
|
.returns(ReadConcern.MAJORITY, from(MongoTransactionOptions::getReadConcern)) //
|
||||||
|
.returns(null, from(MongoTransactionOptions::getReadPreference)) //
|
||||||
|
.returns(null, from(MongoTransactionOptions::getWriteConcern)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // GH-1628
|
||||||
|
public void shouldReturnMergedOptionsIfLabelsContainReadPreference() { |
||||||
|
|
||||||
|
TransactionAttribute attribute = transactionAttribute("mongo:readPreference=primaryPreferred"); |
||||||
|
|
||||||
|
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.get().resolve(attribute)) |
||||||
|
.returns(null, from(TransactionMetadata::getMaxCommitTime)) //
|
||||||
|
.returns(null, from(MongoTransactionOptions::getReadConcern)) //
|
||||||
|
.returns(ReadPreference.primaryPreferred(), from(MongoTransactionOptions::getReadPreference)) //
|
||||||
|
.returns(null, from(MongoTransactionOptions::getWriteConcern)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // GH-1628
|
||||||
|
public void shouldReturnMergedOptionsIfLabelsContainWriteConcern() { |
||||||
|
|
||||||
|
TransactionAttribute attribute = transactionAttribute("mongo:writeConcern=w3"); |
||||||
|
|
||||||
|
assertThat(DefaultMongoTransactionOptionsResolver.INSTANCE.get().resolve(attribute)) |
||||||
|
.returns(null, from(TransactionMetadata::getMaxCommitTime)) //
|
||||||
|
.returns(null, from(MongoTransactionOptions::getReadConcern)) //
|
||||||
|
.returns(null, from(MongoTransactionOptions::getReadPreference)) //
|
||||||
|
.returns(WriteConcern.W3, from(MongoTransactionOptions::getWriteConcern)); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
private static TransactionAttribute transactionAttribute(String... labels) { |
||||||
|
|
||||||
|
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); |
||||||
|
attribute.setLabels(Set.of(labels)); |
||||||
|
return attribute; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue