Restructure the Spring Boot for Apache Geode project to mirror Spring Boot's project structure.

Resolves gh-60.
This commit is contained in:
John Blum
2022-02-24 16:23:35 -08:00
parent 0e8e3baeeb
commit 192133b95e
576 changed files with 0 additions and 19 deletions

View File

@@ -0,0 +1,756 @@
/*
* Copyright 2017-present 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.geode.cache;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
import java.util.function.Predicate;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.DiskStore;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.asyncqueue.AsyncEvent;
import org.apache.geode.cache.asyncqueue.AsyncEventListener;
import org.apache.geode.cache.asyncqueue.AsyncEventQueue;
import org.apache.geode.cache.asyncqueue.AsyncEventQueueFactory;
import org.apache.geode.cache.wan.GatewayEventFilter;
import org.apache.geode.cache.wan.GatewayEventSubstitutionFilter;
import org.apache.geode.cache.wan.GatewaySender;
import org.apache.geode.cache.wan.GatewaySender.OrderPolicy;
import org.springframework.data.gemfire.PeerRegionFactoryBean;
import org.springframework.data.gemfire.config.annotation.RegionConfigurer;
import org.springframework.data.gemfire.util.ArrayUtils;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.data.repository.CrudRepository;
import org.springframework.geode.cache.RepositoryAsyncEventListener.AsyncEventErrorHandler;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A Spring Data for Apache Geode {@link RegionConfigurer} implementation used to configure a target {@link Region}
* to use {@literal Asynchronous Inline Caching} based on the Spring Data {@link CrudRepository Repositories}
* abstraction.
*
* @author John Blum
* @see java.util.function.Function
* @see java.util.function.Predicate
* @see org.apache.geode.cache.Cache
* @see org.apache.geode.cache.Region
* @see org.apache.geode.cache.asyncqueue.AsyncEventListener
* @see org.apache.geode.cache.asyncqueue.AsyncEventQueue
* @see org.apache.geode.cache.asyncqueue.AsyncEventQueueFactory
* @see org.apache.geode.cache.wan.GatewayEventFilter
* @see org.apache.geode.cache.wan.GatewayEventSubstitutionFilter
* @see org.apache.geode.cache.wan.GatewaySender.OrderPolicy
* @see org.springframework.data.gemfire.PeerRegionFactoryBean
* @see org.springframework.data.gemfire.config.annotation.RegionConfigurer
* @see org.springframework.data.repository.CrudRepository
* @see org.springframework.geode.cache.RepositoryAsyncEventListener.AsyncEventErrorHandler
* @since 1.4.0
*/
public class AsyncInlineCachingRegionConfigurer<T, ID> implements RegionConfigurer {
protected static final Predicate<String> DEFAULT_REGION_BEAN_NAME_PREDICATE = beanName -> false;
/**
* Factory method used to construct a new instance of {@link AsyncInlineCachingRegionConfigurer} initialized with
* the given Spring Data {@link CrudRepository} and {@link Predicate} identifying the target {@link Region}
* on which to configure {@literal Asynchronous Inline Caching}.
*
* @param <T> {@link Class type} of the entity.
* @param <ID> {@link Class type} of the identifier, or {@link Region} key.
* @param repository {@link CrudRepository} used to perform data access operations on an external data source
* triggered by cache events and operations on the identified {@link Region}; must not be {@literal null}.
* @param regionBeanName {@link Predicate} used to identify the {@link Region} by {@link String name} on which
* {@literal Asynchronous Inline Caching} will be configured.
* @return a new {@link AsyncInlineCachingRegionConfigurer}.
* @throws IllegalArgumentException if {@link CrudRepository} is {@literal null}.
* @see #AsyncInlineCachingRegionConfigurer(CrudRepository, Predicate)
* @see org.springframework.data.repository.CrudRepository
* @see java.util.function.Predicate
*/
public static <T, ID> AsyncInlineCachingRegionConfigurer<T, ID> create(@NonNull CrudRepository<T, ID> repository,
@Nullable Predicate<String> regionBeanName) {
return new AsyncInlineCachingRegionConfigurer<>(repository, regionBeanName);
}
/**
* Factory method used to construct a new instance of {@link AsyncInlineCachingRegionConfigurer} initialized with
* the given Spring Data {@link CrudRepository} and {@link String} identifying the target {@link Region}
* on which to configure {@literal Asynchronous Inline Caching}.
*
* @param <T> {@link Class type} of the entity.
* @param <ID> {@link Class type} of the identifier, or {@link Region} key.
* @param repository {@link CrudRepository} used to perform data access operations on an external data source
* triggered by cache events and operations on the identified {@link Region}; must not be {@literal null}.
* @param regionBeanName {@link String} used to identify the {@link Region} by {@link String name} on which
* {@literal Asynchronous Inline Caching} will be configured.
* @return a new {@link AsyncInlineCachingRegionConfigurer}.
* @throws IllegalArgumentException if {@link CrudRepository} is {@literal null}.
* @see org.springframework.data.repository.CrudRepository
* @see #create(CrudRepository, Predicate)
* @see java.lang.String
*/
public static <T, ID> AsyncInlineCachingRegionConfigurer<T, ID> create(@NonNull CrudRepository<T, ID> repository,
@Nullable String regionBeanName) {
return create(repository, Predicate.isEqual(regionBeanName));
}
private AsyncEventErrorHandler asyncEventErrorHandler;
private Boolean batchConflationEnabled;
private Boolean diskSynchronous;
private Boolean forwardExpirationDestroy;
private Boolean parallel;
private Boolean persistent;
private Boolean pauseEventDispatching;
private final CrudRepository<T, ID> repository;
private Function<AsyncEventListener, AsyncEventListener> asyncEventListenerPostProcessor;
private Function<AsyncEventQueue, AsyncEventQueue> asyncEventQueuePostProcessor;
private Function<AsyncEventQueueFactory, AsyncEventQueueFactory> asyncEventQueueFactoryPostProcessor;
private Integer batchSize;
private Integer batchTimeInterval;
private Integer dispatcherThreads;
private Integer maximumQueueMemory;
@SuppressWarnings("rawtypes")
private GatewayEventSubstitutionFilter gatewayEventSubstitutionFilter;
private GatewaySender.OrderPolicy orderPolicy;
private List<GatewayEventFilter> gatewayEventFilters;
private final Predicate<String> regionBeanName;
private String diskStoreName;
/**
* Constructs a new instance of {@link AsyncInlineCachingRegionConfigurer} initialized with the given
* {@link CrudRepository} and {@link Predicate} identifying the {@link Region} on which
* {@literal Asynchronous Inline Caching} will be configured.
*
* @param repository {@link CrudRepository} used to perform data access operations on an external data source
* triggered by cache events and operations on the identified {@link Region}; must not be {@literal null}.
* @param regionBeanName {@link Predicate} used to identify the {@link Region} by {@link String name} on which
* {@literal Asynchronous Inline Caching} will be configured.
* @throws IllegalArgumentException if {@link CrudRepository} is {@literal null}.
* @see org.springframework.data.repository.CrudRepository
* @see java.util.function.Predicate
*/
public AsyncInlineCachingRegionConfigurer(@NonNull CrudRepository<T, ID> repository,
@Nullable Predicate<String> regionBeanName) {
Assert.notNull(repository, "CrudRepository must not be null");
this.repository = repository;
this.regionBeanName = regionBeanName != null ? regionBeanName : DEFAULT_REGION_BEAN_NAME_PREDICATE;
}
/**
* Gets the {@link Predicate} identifying the {@link Region} on which {@literal Asynchronous Inline Caching}
* will be configured.
*
* @return the {@link Predicate} used to match the {@link Region} by {@link String name} on which
* {@literal Asynchronous Inline Caching} will be configured; never {@literal null}.
* @see java.util.function.Predicate
*/
protected @NonNull Predicate<String> getRegionBeanName() {
return this.regionBeanName;
}
/**
* Gets the Spring Data {@link CrudRepository} used to perform data access operations on an external data source
* triggered cache events and operations on the target {@link Region}.
*
* @return the Spring Data {@link CrudRepository} used to perform data access operations on an external data source
* triggered cache events and operations on the target {@link Region}; never {@literal null}.
* @see org.springframework.data.repository.CrudRepository
*/
protected @NonNull CrudRepository<T, ID> getRepository() {
return this.repository;
}
/**
* Configures the target {@link Region} by {@link String name} with {@literal Asynchronous Inline Caching}
* functionality.
*
* Effectively, this Configurer creates an {@link AsyncEventQueue} attached to the target {@link Region} with a
* registered {@link RepositoryAsyncEventListener} to perform asynchronous data access operations triggered by
* cache operations to an external, backend data source using a Spring Data {@link CrudRepository}.
*
* @param beanName {@link String} specifying the name of the target {@link Region} and bean name
* in the Spring container.
* @param bean {@link PeerRegionFactoryBean} containing the configuration of the target {@link Region}
* in the Spring container.
* @see org.springframework.data.gemfire.PeerRegionFactoryBean
* @see #newAsyncEventQueue(Cache, String)
*/
@Override
public void configure(String beanName, PeerRegionFactoryBean<?, ?> bean) {
if (getRegionBeanName().test(beanName)) {
AsyncEventQueue queue = newAsyncEventQueue((Cache) bean.getCache(), beanName);
bean.addAsyncEventQueues(ArrayUtils.asArray(queue));
}
}
/**
* Generates a new {@link String ID} for the {@link AsyncEventQueue}.
*
* @param regionBeanName {@link String name} of the target {@link Region}.
* @return a new {@link String ID} for the {@link AsyncEventQueue}.
*/
protected @NonNull String generateId(@NonNull String regionBeanName) {
Assert.hasText(regionBeanName, () -> String.format("Region bean name [%s] must be specified", regionBeanName));
return regionBeanName.concat(String.format("-AEQ-%s", UUID.randomUUID().toString()));
}
/**
* Constructs a new instance of an {@link AsyncEventQueue} to attach to the target {@link Region} configured for
* {@literal Asynchronous Inline Caching}.
*
* @param peerCache reference to the {@link Cache peer cache}; must not be {@literal null}.
* @param regionBeanName {@link String name} of the target {@link Region}; must not be {@literal null}.
* @return a new {@link AsyncEventQueue}.
* @see org.apache.geode.cache.asyncqueue.AsyncEventQueue
* @see org.apache.geode.cache.Cache
* @see #generateId(String)
* @see #newAsyncEventQueueFactory(Cache)
* @see #newAsyncEventQueue(AsyncEventQueueFactory, String, AsyncEventListener)
* @see #newRepositoryAsyncEventListener()
* @see #postProcess(AsyncEventListener)
* @see #postProcess(AsyncEventQueue)
* @see #postProcess(AsyncEventQueueFactory)
*/
protected AsyncEventQueue newAsyncEventQueue(@NonNull Cache peerCache, @NonNull String regionBeanName) {
AsyncEventQueueFactory asyncEventQueueFactory = newAsyncEventQueueFactory(peerCache);
Optional.ofNullable(this.batchConflationEnabled).ifPresent(asyncEventQueueFactory::setBatchConflationEnabled);
Optional.ofNullable(this.batchSize).ifPresent(asyncEventQueueFactory::setBatchSize);
Optional.ofNullable(this.batchTimeInterval).ifPresent(asyncEventQueueFactory::setBatchTimeInterval);
Optional.ofNullable(this.diskStoreName).filter(StringUtils::hasText).ifPresent(asyncEventQueueFactory::setDiskStoreName);
Optional.ofNullable(this.diskSynchronous).ifPresent(asyncEventQueueFactory::setDiskSynchronous);
Optional.ofNullable(this.dispatcherThreads).ifPresent(asyncEventQueueFactory::setDispatcherThreads);
Optional.ofNullable(this.forwardExpirationDestroy).ifPresent(asyncEventQueueFactory::setForwardExpirationDestroy);
Optional.ofNullable(this.gatewayEventSubstitutionFilter).ifPresent(asyncEventQueueFactory::setGatewayEventSubstitutionListener);
Optional.ofNullable(this.maximumQueueMemory).ifPresent(asyncEventQueueFactory::setMaximumQueueMemory);
Optional.ofNullable(this.orderPolicy).ifPresent(asyncEventQueueFactory::setOrderPolicy);
Optional.ofNullable(this.parallel).ifPresent(asyncEventQueueFactory::setParallel);
Optional.ofNullable(this.persistent).ifPresent(asyncEventQueueFactory::setPersistent);
CollectionUtils.nullSafeList(this.gatewayEventFilters).stream()
.filter(Objects::nonNull)
.forEach(asyncEventQueueFactory::addGatewayEventFilter);
if (Boolean.TRUE.equals(this.pauseEventDispatching)) {
asyncEventQueueFactory.pauseEventDispatching();
}
String asyncEventQueueId = generateId(regionBeanName);
AsyncEventListener asyncEventListener = newRepositoryAsyncEventListener();
asyncEventListener = postProcess(asyncEventListener);
asyncEventQueueFactory = postProcess(asyncEventQueueFactory);
AsyncEventQueue asyncEventQueue =
newAsyncEventQueue(asyncEventQueueFactory, asyncEventQueueId, asyncEventListener);
asyncEventQueue = postProcess(asyncEventQueue);
return asyncEventQueue;
}
/**
* Constructs (creates) a new instance of {@link AsyncEventQueue} using the given {@link AsyncEventQueueFactory}
* with the given {@link String AEQ ID} and {@link AsyncEventListener}.
*
* @param factory {@link AsyncEventQueueFactory} used to create the {@link AsyncEventQueue};
* must not be {@literal null}.
* @param asyncEventQueueId {@link String} containing the {@literal ID} for the {@link AsyncEventQueue};
* must not be {@literal null}.
* @param listener {@link AsyncEventListener} registered with the {@link AsyncEventQueue} to process cache events
* from the {@link AsyncEventQueue} attached to the {@link Region}.
* @return a new {@link AsyncEventQueue} with the {@link String ID} and registered {@link AsyncEventListener}.
* @see org.apache.geode.cache.asyncqueue.AsyncEventQueueFactory
* @see org.apache.geode.cache.asyncqueue.AsyncEventQueue
* @see org.apache.geode.cache.asyncqueue.AsyncEventListener
*/
protected @NonNull AsyncEventQueue newAsyncEventQueue(@NonNull AsyncEventQueueFactory factory,
@NonNull String asyncEventQueueId, @NonNull AsyncEventListener listener) {
return factory.create(asyncEventQueueId, listener);
}
/**
* Constructs (creates) a new instance of the {@link AsyncEventQueueFactory} from the given {@literal peer}
* {@link Cache}.
*
* @param peerCache {@literal Peer} {@link Cache} instance used to create an instance of
* the {@link AsyncEventQueueFactory}; must not be {@literal null}.
* @return a new instance of {@link AsyncEventQueueFactory} to create a {@link AsyncEventQueue}.
* @see org.apache.geode.cache.asyncqueue.AsyncEventQueueFactory
* @see org.apache.geode.cache.Cache
*/
protected @NonNull AsyncEventQueueFactory newAsyncEventQueueFactory(@NonNull Cache peerCache) {
return peerCache.createAsyncEventQueueFactory();
}
/**
* Constructs a new Apache Geode {@link AsyncEventListener} to register on an {@link AsyncEventQueue} attached to
* the target {@link Region}, which uses the {@link CrudRepository} to perform data access operations on an external
* backend data source asynchronously when cache events and operations occur on the target {@link Region}.
*
* @return a new {@link RepositoryAsyncEventListener}.
* @see org.springframework.geode.cache.RepositoryAsyncEventListener
* @see org.apache.geode.cache.asyncqueue.AsyncEventListener
* @see #newRepositoryAsyncEventListener(CrudRepository)
* @see #getRepository()
*/
protected @NonNull AsyncEventListener newRepositoryAsyncEventListener() {
return newRepositoryAsyncEventListener(getRepository());
}
/**
* Constructs a new Apache Geode {@link AsyncEventListener} to register on an {@link AsyncEventQueue} attached to
* the target {@link Region}, which uses the given {@link CrudRepository} to perform data access operations on an
* external, backend data source asynchronously when cache events and operations occur on the target {@link Region}.
*
* @param repository Spring Data {@link CrudRepository} used to perform data access operations on the external,
* backend data source; must not be {@literal null}.
* @return a new {@link RepositoryAsyncEventListener}.
* @see org.springframework.geode.cache.RepositoryAsyncEventListener
* @see org.apache.geode.cache.asyncqueue.AsyncEventListener
* @see #getRepository()
*/
protected @NonNull AsyncEventListener newRepositoryAsyncEventListener(@NonNull CrudRepository<T, ID> repository) {
return new RepositoryAsyncEventListener<>(repository);
}
/**
* Applies the user-defined {@link Function} to the framework constructed/provided {@link AsyncEventListener}
* for post processing.
*
* @param asyncEventListener {@link AsyncEventListener} constructed by the framework and post processed by
* end-user code encapsulated in the {@link #applyToListener(Function) configured} {@link Function}.
* @return the post-processed {@link AsyncEventListener}.
* @see org.apache.geode.cache.asyncqueue.AsyncEventListener
* @see #applyToListener(Function)
*/
protected @NonNull AsyncEventListener postProcess(@NonNull AsyncEventListener asyncEventListener) {
return resolveAsyncEventListenerPostProcessor().apply(asyncEventListener);
}
/**
* Applies the user-defined {@link Function} to the framework constructed/provided {@link AsyncEventQueue}
* for post processing.
*
* @param asyncEventQueue {@link AsyncEventQueue} constructed by the framework and post processed by
* end-user code encapsulated in the {@link #applyToQueue(Function) configured} {@link Function}.
* @return the post-processed {@link AsyncEventQueue}.
* @see org.apache.geode.cache.asyncqueue.AsyncEventQueue
* @see #applyToQueue(Function)
*/
protected @NonNull AsyncEventQueue postProcess(@NonNull AsyncEventQueue asyncEventQueue) {
Function<AsyncEventQueue, AsyncEventQueue> asyncEventQueuePostProcessor =
this.asyncEventQueuePostProcessor;
return asyncEventQueuePostProcessor != null
? asyncEventQueuePostProcessor.apply(asyncEventQueue)
: asyncEventQueue;
}
/**
* Applies the user-defined {@link Function} to the framework constructed/provided {@link AsyncEventQueueFactory}
* for post processing.
*
* @param asyncEventQueueFactory {@link AsyncEventQueueFactory} constructed by the framework and post processed by
* end-user code encapsulated in the {@link #applyToQueueFactory(Function) configured} {@link Function}.
* @return the post-processed {@link AsyncEventQueueFactory}.
* @see org.apache.geode.cache.asyncqueue.AsyncEventQueueFactory
* @see #applyToQueueFactory(Function)
*/
protected @NonNull AsyncEventQueueFactory postProcess(@NonNull AsyncEventQueueFactory asyncEventQueueFactory) {
Function<AsyncEventQueueFactory, AsyncEventQueueFactory> asyncEventQueueFactoryPostProcessor =
this.asyncEventQueueFactoryPostProcessor;
return asyncEventQueueFactoryPostProcessor != null
? asyncEventQueueFactoryPostProcessor.apply(asyncEventQueueFactory)
: asyncEventQueueFactory;
}
@SuppressWarnings("unchecked")
private @NonNull Function<AsyncEventListener, AsyncEventListener> resolveAsyncEventListenerPostProcessor() {
AsyncEventErrorHandler asyncEventErrorHandler = this.asyncEventErrorHandler;
Function<AsyncEventListener, AsyncEventListener> resolvedListenerPostProcessor = asyncEventErrorHandler != null
? listener -> {
if (listener instanceof RepositoryAsyncEventListener) {
((RepositoryAsyncEventListener<T, ID>) listener).setAsyncEventErrorHandler(asyncEventErrorHandler);
}
return listener;
}
: Function.identity();
Function<AsyncEventListener, AsyncEventListener> asyncEventListenerPostProcessor =
this.asyncEventListenerPostProcessor;
if (asyncEventListenerPostProcessor != null) {
resolvedListenerPostProcessor = resolvedListenerPostProcessor.andThen(asyncEventListenerPostProcessor);
}
return resolvedListenerPostProcessor;
}
/**
* Builder method used to configure the given user-defined {@link Function} applied to the framework constructed
* and provided {@link AsyncEventListener} for post processing.
*
* @param asyncEventListenerPostProcessor user-defined {@link Function} encapsulating the logic applied to
* the framework constructed/provided {@link AsyncEventListener} for post-processing.
* @return this {@link AsyncInlineCachingRegionConfigurer}.
* @see org.apache.geode.cache.asyncqueue.AsyncEventListener
* @see java.util.function.Function
*/
public AsyncInlineCachingRegionConfigurer<T, ID> applyToListener(
@Nullable Function<AsyncEventListener, AsyncEventListener> asyncEventListenerPostProcessor) {
this.asyncEventListenerPostProcessor = asyncEventListenerPostProcessor;
return this;
}
/**
* Builder method used to configure the given user-defined {@link Function} applied to the framework constructed
* and provided {@link AsyncEventQueue} for post processing.
*
* @param asyncEventQueuePostProcessor user-defined {@link Function} encapsulating the logic applied to
* the framework constructed {@link AsyncEventQueue} for post-processing.
* @return this {@link AsyncInlineCachingRegionConfigurer}.
* @see org.apache.geode.cache.asyncqueue.AsyncEventQueue
* @see java.util.function.Function
*/
public AsyncInlineCachingRegionConfigurer<T, ID> applyToQueue(
@Nullable Function<AsyncEventQueue, AsyncEventQueue> asyncEventQueuePostProcessor) {
this.asyncEventQueuePostProcessor = asyncEventQueuePostProcessor;
return this;
}
/**
* Builder method used to configure the given user-defined {@link Function} applied to the framework constructed
* and provided {@link AsyncEventQueueFactory} for post processing.
*
* @param asyncEventQueueFactoryPostProcessor user-defined {@link Function} encapsulating the logic applied to
* the framework constructed {@link AsyncEventQueueFactory} for post-processing.
* @return this {@link AsyncInlineCachingRegionConfigurer}.
* @see org.apache.geode.cache.asyncqueue.AsyncEventQueueFactory
* @see java.util.function.Function
*/
public AsyncInlineCachingRegionConfigurer<T, ID> applyToQueueFactory(
@Nullable Function<AsyncEventQueueFactory, AsyncEventQueueFactory> asyncEventQueueFactoryPostProcessor) {
this.asyncEventQueueFactoryPostProcessor = asyncEventQueueFactoryPostProcessor;
return this;
}
/**
* Builder method used to configure a {@link AsyncEventErrorHandler} to handle errors thrown while processing
* {@link AsyncEvent AsyncEvents} in the {@link AsyncEventListener}.
*
* @param errorHandler {@link AsyncEventErrorHandler} used to handle errors thrown while processing
* {@link AsyncEvent AsyncEvents} in the {@link AsyncEventListener}.
* @return this {@link AsyncInlineCachingRegionConfigurer}.
* @see org.springframework.geode.cache.RepositoryAsyncEventListener.AsyncEventErrorHandler
*/
public AsyncInlineCachingRegionConfigurer<T, ID> withAsyncEventErrorHandler(
@Nullable AsyncEventErrorHandler errorHandler) {
this.asyncEventErrorHandler = errorHandler;
return this;
}
/**
* Builder method used to enable all {@link AsyncEventQueue AEQs} attached to {@link Region Regions} hosted
* and distributed across the cache cluster to process cache events.
*
* Default is {@literal false}, or {@literal serial}.
*
* @return this {@link AsyncInlineCachingRegionConfigurer}.
* @see #withSerialQueue()
*/
public AsyncInlineCachingRegionConfigurer<T, ID> withParallelQueue() {
this.parallel = true;
return this;
}
/**
* Builder method used to enable the {@link AsyncEventQueue} to persist cache events to disk in order to
* preserve unprocessed cache events while offline.
*
* Keep in mind that the {@link AsyncEventQueue} must be persistent if the data {@link Region}
* to which the AEQ is attached is persistent.
*
* Default is {@literal false}.
*
* @return this {@link AsyncInlineCachingRegionConfigurer}.
*/
public AsyncInlineCachingRegionConfigurer<T, ID> withPersistentQueue() {
this.persistent = true;
return this;
}
/**
* Builder method used to configure the {@link AsyncEventQueue} to conflate cache events in the queue.
*
* When conflation is enabled, the AEQ listener will only receive the latest update in the AEQ for cache entry
* based on key.
*
* Defaults to {@literal false}.
*
* @return this {@link AsyncInlineCachingRegionConfigurer}.
*/
public AsyncInlineCachingRegionConfigurer<T, ID> withQueueBatchConflationEnabled() {
this.batchConflationEnabled = true;
return this;
}
/**
* Builder method used to configure the {@link AsyncEventQueue} {@link Integer batch size}, which determines
* the number (i.e. threshold) of cache events that will trigger the AEQ listener, before any set period of time.
*
* The batch size is often used in tandem with the batch time interval, which determines when the AEQ listener
* will be invoked after a period of time if the batch size is not reached within the period so that cache events
* can also be processed in a timely manner if they are occurring infrequently.
*
* Defaults to {@literal 100}.
*
* @param batchSize the {@link Integer number} of cache events in the queue before the AEQ listener is called.
* @return this {@link AsyncInlineCachingRegionConfigurer}.
* @see #withQueueBatchTimeInterval(Duration)
*/
public AsyncInlineCachingRegionConfigurer<T, ID> withQueueBatchSize(int batchSize) {
this.batchSize = batchSize;
return this;
}
/**
* Builder method used to configure the {@link AsyncEventQueue} {@link Duration batch time interval} determining
* when the AEQ listener will be trigger before the number of cache events reaches any set size.
*
* The {@link Duration} is converted to milliseconds (ms), as expected by the configuration
* of the {@link AsyncEventQueue}.
*
* The batch time interval is often used in tandem with batch size, which determines for how many cache events
* in the queue will trigger the AEQ listener. If cache events are occurring rather frequently, then the batch size
* can help reduce memory consumption by processing the cache events before the batch time interval expires.
*
* Defaults to {@literal 5 ms}.
*
* @param batchTimeInterval {@link Duration} of time to determine when the AEQ listener should be invoked with
* any existing cache events in the queue.
* @return this {@link AsyncInlineCachingRegionConfigurer}.
*/
public AsyncInlineCachingRegionConfigurer<T, ID> withQueueBatchTimeInterval(Duration batchTimeInterval) {
this.batchTimeInterval = batchTimeInterval != null
? Long.valueOf(batchTimeInterval.toMillis()).intValue()
: null;
return this;
}
/**
* Builder method used to configure the {@link String name} of the {@link DiskStore} used by
* the {@link AsyncEventQueue} to persist or overflow cache events.
*
* By default, the AEQ will write cache events to the {@literal DEFAULT} {@link DiskStore}.
*
* @param diskStoreName {@link String name} of the {@link DiskStore}.
* @return this {@link AsyncInlineCachingRegionConfigurer}.
*/
public AsyncInlineCachingRegionConfigurer<T, ID> withQueueDiskStore(String diskStoreName) {
this.diskStoreName = diskStoreName;
return this;
}
/**
* Builder method used to configure the {@link AsyncEventQueue} to perform all disk write operations synchronously.
*
* Default is {@literal true}.
*
* @return this {@link AsyncInlineCachingRegionConfigurer}.
*/
public AsyncInlineCachingRegionConfigurer<T, ID> withQueueDiskSynchronizationEnabled() {
this.diskSynchronous = true;
return this;
}
/**
* Builder method to configure the number of {@link Thread Threads} to process the cache events (contents)
* in the {@link AsyncEventQueue} when the queue is parallel.
*
* When a queue is parallel, the total number of queues is determined by the number of Geode members
* hosting the {@link Region} to which the queue is attached.
*
* When a queue is serial and multiple dispatcher threads are configured, Geode creates an additional copy of
* the queue for each thread on each Geode member that hosts the queue. When the queue is serial and multiple
* dispatcher threads are configure, then you can use the {@link GatewaySender} {@link OrderPolicy} to control
* the distribution of cache events from the queue by the threads.
*
* Default is {@literal 5}.
*
* @param dispatcherThreadCount {@link Integer number} of dispatcher {@link Thread Threads} processing cache events
* in the queue.
* @return this {@link AsyncInlineCachingRegionConfigurer}.
*/
public AsyncInlineCachingRegionConfigurer<T, ID> withQueueDispatcherThreadCount(int dispatcherThreadCount) {
this.dispatcherThreads = dispatcherThreadCount;
return this;
}
/**
* Builder method used to configure whether the {@link AsyncEventQueue} is currently processing cache events
* or is paused.
*
* When paused, cache events will not be dispatched to the AEQ listener for processing. Call the
* {@link AsyncEventQueue#resumeEventDispatching()} to resume cache event processing and AEQ listener callbacks.
*
* @return this {@link AsyncInlineCachingRegionConfigurer}.
*/
public AsyncInlineCachingRegionConfigurer<T, ID> withQueueEventDispatchingPaused() {
this.pauseEventDispatching = true;
return this;
}
/**
* Builder method to configure the {@link AsyncEventQueue} with a {@link List} of
* {@link GatewayEventFilter GatewayEventFilters} to filter cache events sent to the configured AEQ listener.
*
* @param eventFilters {@link List} of {@link GatewayEventFilter GatewayEventFilters} used to control and filter
* the cache events sent to the configured AEQ listener.
* @return this {@link AsyncInlineCachingRegionConfigurer}.
* @see org.apache.geode.cache.wan.GatewayEventFilter
* @see java.util.List
*/
public AsyncInlineCachingRegionConfigurer<T, ID> withQueueEventFilters(List<GatewayEventFilter> eventFilters) {
this.gatewayEventFilters = eventFilters;
return this;
}
/**
* Builder method used to configure the {@link AsyncEventQueue} with a
* {@link GatewayEventSubstitutionFilter cache event substitution filter} used to replace (or "substitute")
* the original cache entry event value enqueued in the AEQ.
*
* @param eventSubstitutionFilter {@link GatewayEventSubstitutionFilter} used to replace/substitute the value
* in the enqueued cache entry event.
* @return this {@link AsyncInlineCachingRegionConfigurer}.
* @see org.apache.geode.cache.wan.GatewayEventSubstitutionFilter
*/
public AsyncInlineCachingRegionConfigurer<T, ID> withQueueEventSubstitutionFilter(
@Nullable GatewayEventSubstitutionFilter<ID, T> eventSubstitutionFilter) {
this.gatewayEventSubstitutionFilter = eventSubstitutionFilter;
return this;
}
/**
* Builder method used to configure whether cache {@link Region} entry destroyed events due to expiration
* are forwarded to the {@link AsyncEventQueue}.
*
* @return this {@link AsyncInlineCachingRegionConfigurer}.
*/
public AsyncInlineCachingRegionConfigurer<T, ID> withQueueForwardedExpirationDestroyEvents() {
this.forwardExpirationDestroy = true;
return this;
}
/**
* Builder method used to configure the maximum JVM Heap memory in megabytes used by the {@link AsyncEventQueue}.
*
* After the maximum memory threshold is reached then the AEQ overflows cache events to disk.
*
* Default to {@literal 100 MB}.
*
* @param maximumMemory {@link Integer} value specifying the maximum amount of memory in megabytes used by the AEQ
* to capture cache events.
* @return this {@link AsyncInlineCachingRegionConfigurer}.
*/
public AsyncInlineCachingRegionConfigurer<T, ID> withQueueMaxMemory(int maximumMemory) {
this.maximumQueueMemory = maximumMemory;
return this;
}
/**
* Builder method used to configure the {@link AsyncEventQueue} order of processing for cache events when the AEQ
* is serial and the AEQ is using multiple dispatcher threads.
*
* @param orderPolicy {@link GatewaySender} {@link OrderPolicy} used to determine the order of processing
* for cache events when the AEQ is serial and uses multiple dispatcher threads.
* @return this {@link AsyncInlineCachingRegionConfigurer}.
* @see org.apache.geode.cache.wan.GatewaySender.OrderPolicy
*/
public AsyncInlineCachingRegionConfigurer<T, ID> withQueueOrderPolicy(
@Nullable GatewaySender.OrderPolicy orderPolicy) {
this.orderPolicy = orderPolicy;
return this;
}
/**
* Builder method used to enable a single {@link AsyncEventQueue AEQ} attached to a {@link Region Region}
* (possibly) hosted and distributed across the cache cluster to process cache events.
*
* Default is {@literal false}, or {@literal serial}.
*
* @return this {@link AsyncInlineCachingRegionConfigurer}.
* @see #withParallelQueue()
*/
public AsyncInlineCachingRegionConfigurer<T, ID> withSerialQueue() {
this.parallel = false;
return this;
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright 2017-present 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.geode.cache;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import org.apache.geode.cache.CacheLoader;
import org.apache.geode.cache.CacheWriter;
import org.apache.geode.cache.Region;
import org.springframework.data.gemfire.PeerRegionFactoryBean;
import org.springframework.data.gemfire.client.ClientRegionFactoryBean;
import org.springframework.data.gemfire.config.annotation.RegionConfigurer;
import org.springframework.data.repository.CrudRepository;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A {@link RegionConfigurer} implementation used to enable Inline Caching on a designated {@link Region}.
*
* @author John Blum
* @see java.util.function.Predicate
* @see org.apache.geode.cache.CacheLoader
* @see org.apache.geode.cache.CacheWriter
* @see org.apache.geode.cache.Region
* @see org.springframework.data.gemfire.config.annotation.RegionConfigurer
* @see org.springframework.geode.cache.RepositoryCacheLoaderRegionConfigurer
* @see org.springframework.geode.cache.RepositoryCacheWriterRegionConfigurer
* @since 1.1.0
*/
public class InlineCachingRegionConfigurer<T, ID> implements RegionConfigurer {
private final List<RegionConfigurer> regionConfigurers = new ArrayList<>();
private final RegionConfigurer compositeRegionConfigurer = new RegionConfigurer() {
@Override
public void configure(String beanName, ClientRegionFactoryBean<?, ?> bean) {
regionConfigurers.forEach(regionConfigurer -> regionConfigurer.configure(beanName, bean));
}
@Override
public void configure(String beanName, PeerRegionFactoryBean<?, ?> bean) {
regionConfigurers.forEach(regionConfigurer -> regionConfigurer.configure(beanName, bean));
}
};
/**
* Constructs a new instance of {@link InlineCachingRegionConfigurer} initialized with
* the given {@link CrudRepository} used for Inline Caching and {@link Predicate} used to identify
* the target {@link Region} on which the {@link CacheLoader} and {@link CacheWriter} will be registered.
*
* @param repository Spring Data {@link CrudRepository} used for Inline Caching between a {@link Region}
* and external data source.
* @param regionBeanName {@link Predicate} identifying the target {@link Region} on which to enable Inline Caching.
* @throws IllegalArgumentException if {@link CrudRepository} is {@literal null}.
* @see org.springframework.data.repository.CrudRepository
* @see java.util.function.Predicate
*/
public InlineCachingRegionConfigurer(@NonNull CrudRepository<T, ID> repository,
@Nullable Predicate<String> regionBeanName) {
Assert.notNull(repository, "CrudRepository is required");
regionBeanName = regionBeanName != null ? regionBeanName : beanName -> false;
this.regionConfigurers.add(newRepositoryCacheLoaderRegionConfigurer(repository, regionBeanName));
this.regionConfigurers.add(newRepositoryCacheWriterRegionConfigurer(repository, regionBeanName));
}
/**
* Constructs a new instance of {@link RepositoryCacheLoaderRegionConfigurer} initialized with
* the given {@link CrudRepository} to load (read-through) {@link Region} values on cache misses
* and {@link Predicate} to identify the target {@link Region} on which to register the {@link CacheLoader}.
*
* @param repository {@link CrudRepository} used to load {@link Region} values on cache misses.
* @param regionBeanName {@link Predicate} used to identify the target {@link Region} on which
* to register the {@link CacheLoader}.
* @return a new {@link RepositoryCacheLoaderRegionConfigurer}.
* @see org.springframework.geode.cache.RepositoryCacheLoaderRegionConfigurer
* @see org.springframework.data.repository.CrudRepository
* @see java.util.function.Predicate
*/
protected RepositoryCacheLoaderRegionConfigurer<T, ID> newRepositoryCacheLoaderRegionConfigurer(
@NonNull CrudRepository<T, ID> repository, @Nullable Predicate<String> regionBeanName) {
return new RepositoryCacheLoaderRegionConfigurer<>(repository, regionBeanName);
}
/**
* Constructs a new instance of {@link RepositoryCacheWriterRegionConfigurer} initialized with
* the given {@link CrudRepository} to write-through to an external data source and {@link Predicate}
* to identify the target {@link Region} on which to register the {@link CacheWriter}.
*
* @param repository {@link CrudRepository} used to write-through to the external data source.
* @param regionBeanName {@link Predicate} used to identify the target {@link Region} on which
* to register the {@link CacheWriter}.
* @return a new {@link RepositoryCacheWriterRegionConfigurer}.
* @see org.springframework.geode.cache.RepositoryCacheWriterRegionConfigurer
* @see org.springframework.data.repository.CrudRepository
* @see java.util.function.Predicate
*/
protected RepositoryCacheWriterRegionConfigurer<T, ID> newRepositoryCacheWriterRegionConfigurer(
@NonNull CrudRepository<T, ID> repository, @Nullable Predicate<String> regionBeanName) {
return new RepositoryCacheWriterRegionConfigurer<>(repository, regionBeanName);
}
@Override
public void configure(String beanName, ClientRegionFactoryBean<?, ?> bean) {
this.compositeRegionConfigurer.configure(beanName, bean);
}
@Override
public void configure(String beanName, PeerRegionFactoryBean<?, ?> bean) {
this.compositeRegionConfigurer.configure(beanName, bean);
}
}

View File

@@ -0,0 +1,604 @@
/*
* Copyright 2017-present 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.geode.cache;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import org.apache.geode.cache.Operation;
import org.apache.geode.cache.asyncqueue.AsyncEvent;
import org.apache.geode.cache.asyncqueue.AsyncEventListener;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.data.repository.CrudRepository;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* An Apache Geode {@link AsyncEventListener} that uses a Spring Data {@link CrudRepository} to perform
* data access operations to a backend, external data source asynchronously, triggered by cache operations.
*
* @author John Blum
* @see java.util.function.Function
* @see org.apache.geode.cache.Operation
* @see org.apache.geode.cache.asyncqueue.AsyncEvent
* @see org.apache.geode.cache.asyncqueue.AsyncEventListener
* @see org.springframework.data.repository.CrudRepository
* @since 1.4.0
*/
public class RepositoryAsyncEventListener<T, ID> implements AsyncEventListener {
protected static final AsyncEventErrorHandler DEFAULT_ASYNC_EVENT_ERROR_HANDLER = eventError -> false;
private AsyncEventErrorHandler asyncEventErrorHandler = DEFAULT_ASYNC_EVENT_ERROR_HANDLER;
private final AtomicBoolean hasFired = new AtomicBoolean(false);
private final AtomicLong firedCount = new AtomicLong(0L);
private final CrudRepository<T, ID> repository;
private final List<AsyncEventOperationRepositoryFunction<T, ID>> repositoryFunctions = new CopyOnWriteArrayList<>();
/**
* Constructs a new instance of {@link RepositoryAsyncEventListener} initialized with the given Spring Data
* {@link CrudRepository}.
*
* @param repository Spring Data {@link CrudRepository} used to perform data access operations to a backend,
* external data source when triggered by a cache operation; must not be {@literal null}.
* @throws IllegalArgumentException if {@link CrudRepository} is {@literal null}.
* @see org.springframework.data.repository.CrudRepository
*/
public RepositoryAsyncEventListener(@NonNull CrudRepository<T, ID> repository) {
Assert.notNull(repository, "CrudRepository must not be null");
this.repository = repository;
this.repositoryFunctions.addAll(Arrays.asList(
new CreateUpdateAsyncEventRepositoryFunction<>(this),
new RemoveAsyncEventRepositoryFunction<>(this)
));
}
/**
* Determines whether this listener has (ever) been fired (triggered) by the GemFire/Geode AEQ system.
*
* @return a boolean value indicating whether this listener has been fired (triggered).
* @see #hasFiredSinceLastCheck()
*/
@SuppressWarnings("unused")
public boolean hasFired() {
return getFiredCount() > 0;
}
/**
* Determines whether this listener has been fired (triggered) by the GemFire/Geode AEQ system
* since the last check.
*
* A call to this method clears the flag.
*
* @return a boolean value indicating whether this listener has been fired (triggered) since the last check.
* @see #hasFired()
*/
@SuppressWarnings("unused")
public boolean hasFiredSinceLastCheck() {
return this.hasFired.compareAndSet(true, false);
}
/**
* Determines how many times this listener has been fired (triggered) by the GemFire/Geode AEQ system.
*
* @return a {@link Long} value indicating how many times this listener has been fired (triggered).
*/
@SuppressWarnings("unused")
public long getFiredCount() {
return this.firedCount.get();
}
/**
* Configures an {@link AsyncEventErrorHandler} to handle errors that may occur when this listener is invoked with
* a batch of {@link AsyncEvent AsyncEvents}.
*
* Since the processing of {@link AsyncEvent AsyncEvents} is asynchronous, the {@link AsyncEventErrorHandler} gives
* users the opportunity to respond to errors for each {@link AsyncEvent} as it is is processed given this listener
* is designed to coordinate data/state changes occurring in an Apache Geode cache with an external data source.
*
* @param asyncEventErrorHandler {@link AsyncEventErrorHandler} used to handle errors while processing the batch of
* {@link AsyncEvent AsyncEvents}.
* @see AsyncEventErrorHandler
*/
public void setAsyncEventErrorHandler(@Nullable AsyncEventErrorHandler asyncEventErrorHandler) {
this.asyncEventErrorHandler = asyncEventErrorHandler;
}
/**
* Gets the configured {@link AsyncEventErrorHandler} used to handle errors that may occur when this listener
* is invoked with a batch of {@link AsyncEvent AsyncEvents}.
*
* Defaults to an {@link AsyncEventErrorHandler} that always returns {@literal false} on any error.
*
* @return the configured {@link AsyncEventErrorHandler}; never {@literal null}.
* @see AsyncEventErrorHandler
*/
protected @NonNull AsyncEventErrorHandler getAsyncEventErrorHandler() {
return this.asyncEventErrorHandler != null ? this.asyncEventErrorHandler : DEFAULT_ASYNC_EVENT_ERROR_HANDLER;
}
/**
* Gets a reference to the configured Spring Data {@link CrudRepository} used by this {@link AsyncEventListener}
* to perform data access operations to a external, backend data source asynchronously when triggered by a cache
* operation.
*
* @return a reference to the configured Spring Data {@link CrudRepository}; never {@literal null}.
* @see org.springframework.data.repository.CrudRepository
*/
protected @NonNull CrudRepository<T, ID> getRepository() {
return this.repository;
}
/**
* Gets a {@link List} of {@link AsyncEventOperationRepositoryFunction} objects used to process
* {@link AsyncEvent AsyncEvents} passed to this listener by inspecting the {@link Operation}
* on the {@link AsyncEvent} and calling the appropriate {@link CrudRepository} method.
*
* @return a {@link List} of {@link AsyncEventOperationRepositoryFunction} objects to process
* the {@link AsyncEvent AsyncEvents}; never {@literal null}.
* @see AsyncEventOperationRepositoryFunction
*/
protected @NonNull List<AsyncEventOperationRepositoryFunction<T, ID>> getRepositoryFunctions() {
return this.repositoryFunctions;
}
/**
* Processes each {@link AsyncEvent} in order by first determining whether the {@link AsyncEvent} can be processed
* by this listener and then invokes the appropriate Spring Data {@link CrudRepository} data access operation
* corresponding to the {@link AsyncEvent} {@link Operation}.
*
* @param events {@link List} of {@link AsyncEvent AsyncEvents} to process.
* @return a boolean value indicating whether all {@link AsyncEvent AsyncEvents} were processed successfully
* by this listener.
* If any {@link AsyncEvent} fails to be processed (just one), then this method will return {@literal false}.
* If any {@link AsyncEvent} cannot be handled, then this method will return {@literal false}, even if other
* {@link AsyncEvent AsyncEvents} were successfully processed.
* @see org.apache.geode.cache.asyncqueue.AsyncEvent
* @see AsyncEventOperationRepositoryFunction
* @see #getRepositoryFunctions()
* @see java.util.List
*/
@Override
public final boolean processEvents(List<AsyncEvent> events) {
try {
return doProcessEvents(events);
}
finally {
this.firedCount.incrementAndGet();
this.hasFired.set(true);
}
}
/**
* @see #processEvents(List)
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
protected boolean doProcessEvents(List<AsyncEvent> events) {
AtomicBoolean result = new AtomicBoolean(true);
CollectionUtils.nullSafeList(events).stream()
.filter(Objects::nonNull)
.forEach(event -> {
Optional<AsyncEventOperationRepositoryFunction<T, ID>> repositoryFunction =
getRepositoryFunctions().stream()
.filter(function -> function.canProcess(event))
.findFirst();
boolean processed = Boolean.TRUE.equals(repositoryFunction
.map(function -> function.apply(event))
.orElse(false));
result.compareAndSet(true, processed);
});
return result.get();
}
/**
* Registers a {@link AsyncEventOperationRepositoryFunction} capable of processing {@link AsyncEvent AsyncEvents}
* by {@link Operation} and invoking the appropriate Spring Data {@link CrudRepository} data access operation.
*
* {@link AsyncEventOperationRepositoryFunction AsyncEventOperationRepositoryFunctions} can be registered for
* {@link AsyncEvent} {@link Operation Operations} not currently handled by this listener. Alternatively, users can
* override existing {@link AsyncEventOperationRepositoryFunction AsyncEventOperationRepositoryFunctions} provided
* by this listener to alter the default behavior, or effectively the Spring Data {@link CrudRepository} data access
* operation invoked based on the {@link AsyncEvent} {@link Operation}. The {@code repositoryFunction} arguments are
* prepended to the {@link List} of registered {@link Function Functions} to implement the override, where the first
* {@link Function} found capable of handling the {@link AsyncEvent} {@link Operation} will be applied.
*
* @param repositoryFunction {@link AsyncEventOperationRepositoryFunction} used to process
* {@link AsyncEvent AsyncEvents} by {@link Operation} invoking the appropriate Spring Data {@link CrudRepository}
* data access operation; must not be {@literal null}.
* @return a boolean value indicating whether the registration was successful.
* @see AsyncEventOperationRepositoryFunction
* @see #getRepositoryFunctions()
*/
public boolean register(@NonNull AsyncEventOperationRepositoryFunction<T, ID> repositoryFunction) {
if (repositoryFunction != null) {
getRepositoryFunctions().add(0, repositoryFunction);
return true;
}
return false;
}
/**
* Unregisters the given {@link AsyncEventOperationRepositoryFunction} from this listener.
*
* @param repositoryFunction {@link AsyncEventOperationRepositoryFunction} to unregister.
* @return a boolean value indicating whether the un-registration was successful.
* @see AsyncEventOperationRepositoryFunction
* @see #getRepositoryFunctions()
*/
public boolean unregister(@Nullable AsyncEventOperationRepositoryFunction<T, ID> repositoryFunction) {
return getRepositoryFunctions().remove(repositoryFunction);
}
/**
* {@link AsyncEventError} is a wrapper class encapsulating the {@link AsyncEvent} along with
* the {@link Throwable error} that was thrown while processing the event.
*
* @see org.apache.geode.cache.asyncqueue.AsyncEvent
* @see java.lang.Throwable
*/
public static class AsyncEventError {
private final AsyncEvent<?, ?> event;
private final Throwable cause;
/**
* Constructs a new instance of {@link AsyncEventError} initialized with the required {@link AsyncEvent}
* and {@link Throwable} thrown while processing the event.
*
* @param event processed {@link AsyncEvent}; must not be {@literal null}.
* @param cause {@link Throwable error} thrown while processing the event; must not be {@literal null}.
* @throws IllegalArgumentException if the {@link AsyncEvent} or the {@link Throwable} are {@literal null}.
* @see org.apache.geode.cache.asyncqueue.AsyncEvent
* @see java.lang.Throwable
*/
public AsyncEventError(@NonNull AsyncEvent<?, ?> event, @NonNull Throwable cause) {
Assert.notNull(event, "AsyncEvent must not be null");
Assert.notNull(cause, "Cause must not be null");
this.event = event;
this.cause = cause;
}
/**
* Gets the {@link Throwable} thrown while processing the {@link AsyncEvent}.
*
* @return the {@link Throwable} thrown while processing the {@link AsyncEvent}.
* @see java.lang.Throwable
*/
public @NonNull Throwable getCause() {
return this.cause;
}
/**
* Gets the {@link AsyncEvent} being processed when the {@link Throwable error} occurred.
*
* @return the {@link AsyncEvent} being processed when the {@link Throwable error} occurred.
* @see org.apache.geode.cache.asyncqueue.AsyncEvent
*/
public @NonNull AsyncEvent<?, ?> getEvent() {
return this.event;
}
/**
* @inheritDoc
*/
@Override
public String toString() {
return String.format("Error [%s] thrown when processing AsyncEvent [%s]",
getCause().getMessage(), getEvent());
}
}
/**
* The {@link AsyncEventErrorHandler} interface is a {@link Function} and {@link FunctionalInterface} used to
* handle errors while processing {@link AsyncEvent AsyncEvents}.
*
* @see java.lang.FunctionalInterface
* @see java.util.function.Function
* @see AsyncEventError
*/
@FunctionalInterface
public interface AsyncEventErrorHandler extends Function<AsyncEventError, Boolean> { }
/**
* The {@link AsyncEventOperationRepositoryFunction} interface is a {@link Function} and {@link FunctionalInterface}
* that translates the {@link AsyncEvent} {@link Operation} into a Spring Data {@link CrudRepository} method
* invocation.
*
* @param <T> {@link Class type} of the entity tied to the event.
* @param <ID> {@link Class type} of the identifier of the entity.
* @see org.apache.geode.cache.asyncqueue.AsyncEvent
* @see java.lang.FunctionalInterface
* @see java.util.function.Function
*/
@FunctionalInterface
public interface AsyncEventOperationRepositoryFunction<T, ID> extends Function<AsyncEvent<ID, T>, Boolean> {
/**
* Determines whether the given {@link AsyncEvent} can be processed by this {@link Function}.
*
* Implementing classes must override this method to specify which {@link AsyncEvent}
* {@link Operation Operations} they are capable of processing.
*
* @param event {@link AsyncEvent} to evaluate.
* @return a boolean value indicating whether this {@link Function} is capable of processing
* the given {@link AsyncEvent}. Default returns {@literal false}.
* @see org.apache.geode.cache.asyncqueue.AsyncEvent
*/
default boolean canProcess(@Nullable AsyncEvent<ID, T> event) {
return false;
}
}
/**
* {@link AbstractAsyncEventOperationRepositoryFunction} is an abstract base class implementing the
* {@link AsyncEventOperationRepositoryFunction} interface to provided a default {@literal template} implementation
* of the {@link Function#apply(Object)} method.
*
* @param <T> {@link Class type} of the entity tied to the event.
* @param <ID> {@link Class type} of the identifier of the entity.
* @see AsyncEventOperationRepositoryFunction
*/
public static abstract class AbstractAsyncEventOperationRepositoryFunction<T, ID>
implements AsyncEventOperationRepositoryFunction<T, ID> {
private final RepositoryAsyncEventListener<T, ID> listener;
/**
* Constructs an new instance of {@link AbstractAsyncEventOperationRepositoryFunction} initialized with
* the given, required {@link RepositoryAsyncEventListener} to which this function is associated.
*
* @param listener {@link RepositoryAsyncEventListener} processing {@link AsyncEvent AsyncEvents}
* by invoking this {@link Function} to handle them.
* @throws IllegalArgumentException if {@link RepositoryAsyncEventListener} is {@literal null}.
* @see RepositoryAsyncEventListener
*/
public AbstractAsyncEventOperationRepositoryFunction(@NonNull RepositoryAsyncEventListener<T, ID> listener) {
Assert.notNull(listener, "RepositoryAsyncEventListener must not be null");
this.listener = listener;
}
/**
* Alias to the {@link RepositoryAsyncEventListener#getAsyncEventErrorHandler() configured}
* {@link RepositoryAsyncEventListener} {@link AsyncEventErrorHandler}.
*
* @return the configured {@link AsyncEventErrorHandler}; never {@literal null}.
* @see RepositoryAsyncEventListener#getAsyncEventErrorHandler()
* @see AsyncEventErrorHandler
* @see #getListener()
*/
protected AsyncEventErrorHandler getErrorHandler() {
return getListener().getAsyncEventErrorHandler();
}
/**
* Returns a reference to the associated {@link RepositoryAsyncEventListener}.
*
* @return a reference to the associated {@link RepositoryAsyncEventListener}; never {@literal null}.
* @see RepositoryAsyncEventListener
*/
protected @NonNull RepositoryAsyncEventListener<T, ID> getListener() {
return this.listener;
}
/**
* Alias to the {@link RepositoryAsyncEventListener#getRepository() configured}
* {@link RepositoryAsyncEventListener} {@link CrudRepository}.
*
* @return the configured {@link CrudRepository}; never {@literal null}.
* @see org.springframework.data.repository.CrudRepository
* @see RepositoryAsyncEventListener#getRepository()
* @see #getListener()
*/
protected @NonNull CrudRepository<T, ID> getRepository() {
return getListener().getRepository();
}
/**
* Processes the given {@link AsyncEvent} by first determining whether the event can be processed by this
* {@link Function}, and then proceeds to extract the {@link AsyncEvent#getDeserializedValue() entity}
* associated with the event to invoke the appropriate Spring Data {@link CrudRepository} data access operation
* determined by the {@link AsyncEvent} {@link Operation}.
*
* If an {@link Throwable error} is thrown while processing the {@link AsyncEvent}, then the
* {@link AsyncEventErrorHandler} is called to handle the error and perform any necessary/required
* post-processing actions.
*
* {@link AsyncEventErrorHandler} can be implemented to retry the operation with incremental backoff, based on
* count or time, record the failure, perform resource cleanup actions, whatever is necessary and appropriate
* to the application use case.
*
* @param event {@link AsyncEvent} to process.
* @return a boolean value indicating whether the event was successfully processed.
* @throws IllegalStateException if the resolve entity is {@literal null}.
* @see org.apache.geode.cache.asyncqueue.AsyncEvent
* @see AsyncEventErrorHandler
* @see #canProcess(AsyncEvent)
* @see #doRepositoryOp(Object)
* @see #resolveEntity(AsyncEvent)
* @see #getErrorHandler()
*/
@Override
public Boolean apply(@Nullable AsyncEvent<ID, T> event) {
try {
if (canProcess(event)) {
T entity = resolveEntity(event);
doRepositoryOp(entity);
return true;
}
return false;
}
catch (Throwable cause) {
return getErrorHandler().apply(new AsyncEventError(event, cause));
}
}
/**
* Invokes the appropriate Spring Data {@link CrudRepository} data access operation based on the
* {@link AsyncEvent} {@link Operation} as determined by {@link AsyncEvent#getOperation()}.
*
* @param <R> {@link Class type} of the Spring Data {@link CrudRepository} data access operation return value.
* @param entity entity to process.
* @return the result of invoking the Spring Data {@link CrudRepository} data access operation.
* @see org.springframework.data.repository.CrudRepository
*/
protected abstract <R> R doRepositoryOp(@NonNull T entity);
/**
* Resolves the {@link AsyncEvent#getDeserializedValue() entity} associated with the {@link AsyncEvent}.
*
* @param event {@link AsyncEvent} from which to resolve the entity.
* @return the resolve entity from the {@link AsyncEvent}.
* @throws IllegalArgumentException if {@link AsyncEvent} is {@literal null}.
* @throws IllegalStateException if the resolved {@link AsyncEvent#getDeserializedValue() entity}
* is {@literal null}.
* @see org.apache.geode.cache.asyncqueue.AsyncEvent#getDeserializedValue()
* @see org.apache.geode.cache.asyncqueue.AsyncEvent
*/
protected T resolveEntity(@NonNull AsyncEvent<ID, T> event) {
Assert.notNull(event, "AsyncEvent must not be null");
T entity = event.getDeserializedValue();
Assert.state(entity != null, "The entity (deserialized value) was null");
return entity;
}
}
/**
* An {@link AsyncEventOperationRepositoryFunction} capable of handling {@link Operation#CREATE}
* and {@link Operation#UPDATE} {@link AsyncEvent AsyncEvents}.
*
* Invokes the {@link CrudRepository#save(Object)} data access operation.
*
* @param <T> {@link Class type} of the entity tied to the event.
* @param <ID> {@link Class type} of the identifier of the entity.
*/
public static class CreateUpdateAsyncEventRepositoryFunction<T, ID>
extends AbstractAsyncEventOperationRepositoryFunction<T, ID> {
/**
* Constructs a new instance of {@link CreateUpdateAsyncEventRepositoryFunction} initialized with the given,
* required {@link RepositoryAsyncEventListener}.
*
* @param listener {@link RepositoryAsyncEventListener} forwarding {@link AsyncEvent AsyncEvents} for processing
* by this {@link Function}
* @see RepositoryAsyncEventListener
*/
public CreateUpdateAsyncEventRepositoryFunction(@NonNull RepositoryAsyncEventListener<T, ID> listener) {
super(listener);
}
/**
* @inheritDoc
*/
@Override
public boolean canProcess(@Nullable AsyncEvent<ID, T> event) {
Operation operation = event != null ? event.getOperation() : null;
return operation != null && (operation.isCreate() || operation.isUpdate());
}
/**
* @inheritDoc
*/
@Override
@SuppressWarnings("unchecked")
protected <R> R doRepositoryOp(T entity) {
return (R) getRepository().save(entity);
}
}
/**
* An {@link Function} implementation capable of handling {@link Operation#REMOVE} {@link AsyncEvent AsyncEvents}.
*
* Invokes the {@link CrudRepository#delete(Object)} data access operation.
*
* @param <T> {@link Class type} of the entity tied to the event.
* @param <ID> {@link Class type} of the identifier of the entity.
*/
public static class RemoveAsyncEventRepositoryFunction<T, ID>
extends AbstractAsyncEventOperationRepositoryFunction<T, ID> {
/**
* Constructs a new instance of {@link RemoveAsyncEventRepositoryFunction} initialized with the given, required
* {@link RepositoryAsyncEventListener}.
*
* @param listener {@link RepositoryAsyncEventListener} forwarding {@link AsyncEvent AsyncEvents} for processing
* by this {@link Function}
* @see RepositoryAsyncEventListener
*/
public RemoveAsyncEventRepositoryFunction(@NonNull RepositoryAsyncEventListener<T, ID> listener) {
super(listener);
}
/**
* @inheritDoc
*/
@Override
public boolean canProcess(@Nullable AsyncEvent<ID, T> event) {
Operation operation = event != null ? event.getOperation() : null;
return Operation.REMOVE.equals(operation);
}
/**
* @inheritDoc
*/
@Override
protected <R> R doRepositoryOp(T entity) {
getRepository().delete(entity);
return null;
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2017-present 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.geode.cache;
import java.util.function.Supplier;
import org.apache.geode.cache.CacheLoader;
import org.apache.geode.cache.CacheLoaderException;
import org.apache.geode.cache.CacheRuntimeException;
import org.apache.geode.cache.LoaderHelper;
import org.springframework.data.repository.CrudRepository;
import org.springframework.geode.cache.support.RepositoryCacheLoaderWriterSupport;
/**
* A {@link CacheLoader} implementation backed by a Spring Data {@link CrudRepository} used to load an entity
* from an external data source.
*
* @author John Blum
* @see org.apache.geode.cache.CacheLoader
* @see org.springframework.data.repository.CrudRepository
* @see org.springframework.geode.cache.support.CacheLoaderSupport
* @since 1.1.0
*/
@SuppressWarnings("unused")
public class RepositoryCacheLoader<T, ID> extends RepositoryCacheLoaderWriterSupport<T, ID> {
protected static final String CACHE_LOAD_EXCEPTION_MESSAGE = "Error while loading Entity [%s] with Repository [%s]";
public RepositoryCacheLoader(CrudRepository<T, ID> repository) {
super(repository);
}
@Override
public T load(LoaderHelper<ID, T> helper) throws CacheLoaderException {
try {
return getRepository().findById(helper.getKey()).orElse(null);
}
catch (Exception cause) {
throw newCacheRuntimeException(() -> String.format(CACHE_LOAD_EXCEPTION_MESSAGE,
helper.getKey(), getRepository().getClass().getName()), cause);
}
}
@Override
protected CacheRuntimeException newCacheRuntimeException(Supplier<String> messageSupplier, Throwable cause) {
return new CacheLoaderException(messageSupplier.get(), cause);
}
}

View File

@@ -0,0 +1,176 @@
/*
* Copyright 2017-present 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.geode.cache;
import java.util.function.Predicate;
import org.apache.geode.cache.CacheLoader;
import org.apache.geode.cache.Region;
import org.springframework.data.gemfire.PeerRegionFactoryBean;
import org.springframework.data.gemfire.client.ClientRegionFactoryBean;
import org.springframework.data.gemfire.config.annotation.RegionConfigurer;
import org.springframework.data.repository.CrudRepository;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Spring Data {@link RegionConfigurer} implementation used to adapt and register a Spring Data {@link CrudRepository}
* as a {@link CacheLoader} for a targeted {@link Region}.
*
* @author John Blum
* @param <T> {@link Class type} of the persistent entity.
* @param <ID> {@link Class type} of the persistent entity identifier (ID).
* @see java.util.function.Predicate
* @see org.apache.geode.cache.CacheLoader
* @see org.apache.geode.cache.Region
* @see org.springframework.data.gemfire.PeerRegionFactoryBean
* @see org.springframework.data.gemfire.client.ClientRegionFactoryBean
* @see org.springframework.data.gemfire.config.annotation.RegionConfigurer
* @see org.springframework.data.repository.CrudRepository
* @since 1.1.0
*/
public class RepositoryCacheLoaderRegionConfigurer<T, ID> implements RegionConfigurer {
/**
* Factory method used to construct a new instance of {@link RepositoryCacheLoaderRegionConfigurer} initialized with
* the given Spring Data {@link CrudRepository} used to load {@link Region} values on cache misses as well as
* the given {@link Predicate} used to identify/qualify the {@link Region} on which the {@link CrudRepository}
* will be registered and used as a {@link CacheLoader}.
*
* @param <T> {@link Class type} of the persistent entity.
* @param <ID> {@link Class type} of the persistent entity identifier (ID).
* @param repository {@link CrudRepository} used to load {@link Region} values on cache misses.
* @param regionBeanName {@link Predicate} used to identify/qualify the {@link Region} on which
* the {@link CrudRepository} will be registered and used as a {@link CacheLoader}.
* @return a new instance of {@link RepositoryCacheLoaderRegionConfigurer}.
* @throws IllegalArgumentException if {@link CrudRepository} is {@literal null}.
* @see org.springframework.data.repository.CrudRepository
* @see java.util.function.Predicate
* @see #RepositoryCacheLoaderRegionConfigurer(CrudRepository, Predicate)
*/
public static <T, ID> RepositoryCacheLoaderRegionConfigurer<T, ID> create(@NonNull CrudRepository<T, ID> repository,
@Nullable Predicate<String> regionBeanName) {
return new RepositoryCacheLoaderRegionConfigurer<>(repository, regionBeanName);
}
/**
* Factory method used to construct a new instance of {@link RepositoryCacheLoaderRegionConfigurer} initialized with
* the given Spring Data {@link CrudRepository} used to load {@link Region} values on cache misses as well as
* the given {@link String} identifying/qualifying the {@link Region} on which the {@link CrudRepository}
* will be registered and used as a {@link CacheLoader}.
*
* @param <T> {@link Class type} of the persistent entity.
* @param <ID> {@link Class type} of the persistent entity identifier (ID).
* @param repository {@link CrudRepository} used to load {@link Region} values on cache misses.
* @param regionBeanName {@link String} containing the bean name identifying/qualifying the {@link Region}
* on which the {@link CrudRepository} will be registered and used as a {@link CacheLoader}.
* @return a new instance of {@link RepositoryCacheLoaderRegionConfigurer}.
* @throws IllegalArgumentException if {@link CrudRepository} is {@literal null}.
* @see org.springframework.data.repository.CrudRepository
* @see java.lang.String
* @see #create(CrudRepository, Predicate)
*/
public static <T, ID> RepositoryCacheLoaderRegionConfigurer<T, ID> create(@NonNull CrudRepository<T, ID> repository,
@Nullable String regionBeanName) {
return create(repository, Predicate.isEqual(regionBeanName));
}
private final CrudRepository<T, ID> repository;
private final Predicate<String> regionBeanName;
/**
* Constructs a new instance of {@link RepositoryCacheLoaderRegionConfigurer} initialized with the given Spring Data
* {@link CrudRepository} used to load {@link Region} values on cache misses as well as the given {@link Predicate}
* used to identify/qualify the {@link Region} on which the {@link CrudRepository} will be registered
* and used as a {@link CacheLoader}.
*
* @param repository {@link CrudRepository} used to load {@link Region} values on cache misses.
* @param regionBeanName {@link Predicate} used to identify/qualify the {@link Region} on which
* the {@link CrudRepository} will be registered and used as a {@link CacheLoader}.
* @throws IllegalArgumentException if {@link CrudRepository} is {@literal null}.
* @see org.springframework.data.repository.CrudRepository
* @see java.util.function.Predicate
*/
public RepositoryCacheLoaderRegionConfigurer(@NonNull CrudRepository<T, ID> repository,
@Nullable Predicate<String> regionBeanName) {
Assert.notNull(repository, "CrudRepository is required");
this.repository = repository;
this.regionBeanName = regionBeanName != null ? regionBeanName : beanName -> false;
}
/**
* Returns the configured {@link Predicate} used to identify/qualify the {@link Region}
* on which the {@link CrudRepository} will be registered as a {@link CacheLoader} for cache misses.
*
* @return the configured {@link Predicate} used to identify/qualify the {@link Region}
* targeted for the {@link CacheLoader} registration.
* @see java.util.function.Predicate
*/
protected @NonNull Predicate<String> getRegionBeanName() {
return this.regionBeanName;
}
/**
* Returns the configured Spring Data {@link CrudRepository} adapted/wrapped as a {@link CacheLoader}
* and used to load {@link Region} values on cache misses.
*
* @return the configured {@link CrudRepository} used to load {@link Region} values on cache misses.
* @see org.springframework.data.repository.CrudRepository
*/
protected @NonNull CrudRepository<T, ID> getRepository() {
return this.repository;
}
@Override
@SuppressWarnings("unchecked")
public void configure(String beanName, ClientRegionFactoryBean<?, ?> bean) {
if (getRegionBeanName().test(beanName)) {
bean.setCacheLoader(newRepositoryCacheLoader());
}
}
@Override
@SuppressWarnings("unchecked")
public void configure(String beanName, PeerRegionFactoryBean<?, ?> bean) {
if (getRegionBeanName().test(beanName)) {
bean.setCacheLoader(newRepositoryCacheLoader());
}
}
/**
* Constructs a new instance of {@link RepositoryCacheLoader} adapting the {@link CrudRepository}
* as an instance of a {@link CacheLoader}.
*
* @return a new {@link RepositoryCacheLoader}.
* @see org.springframework.geode.cache.RepositoryCacheLoader
* @see org.springframework.data.repository.CrudRepository
* @see org.apache.geode.cache.CacheLoader
* @see #getRepository()
*/
@SuppressWarnings("rawtypes")
protected RepositoryCacheLoader newRepositoryCacheLoader() {
return new RepositoryCacheLoader<>(getRepository());
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2017-present 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.geode.cache;
import java.util.function.Supplier;
import org.apache.geode.cache.CacheRuntimeException;
import org.apache.geode.cache.CacheWriter;
import org.apache.geode.cache.CacheWriterException;
import org.apache.geode.cache.EntryEvent;
import org.apache.geode.cache.RegionEvent;
import org.springframework.data.repository.CrudRepository;
import org.springframework.geode.cache.support.RepositoryCacheLoaderWriterSupport;
import org.springframework.geode.core.util.function.FunctionUtils;
/**
* A {@link CacheWriter} implementation backed by a Spring Data {@link CrudRepository} used to persist a cache entry
* (i.e. entity) to a backend, external data source.
*
* @author John Blum
* @see org.apache.geode.cache.CacheWriter
* @see org.springframework.data.repository.CrudRepository
* @see org.springframework.geode.cache.support.RepositoryCacheLoaderWriterSupport
* @since 1.1.0
*/
@SuppressWarnings("unused")
public class RepositoryCacheWriter<T, ID> extends RepositoryCacheLoaderWriterSupport<T, ID> {
public RepositoryCacheWriter(CrudRepository<T, ID> repository) {
super(repository);
}
@Override
public void beforeCreate(EntryEvent<ID, T> event) throws CacheWriterException {
doRepositoryOp(event.getNewValue(), getRepository()::save);
}
@Override
public void beforeUpdate(EntryEvent<ID, T> event) throws CacheWriterException {
doRepositoryOp(event.getNewValue(), getRepository()::save);
}
@Override
public void beforeDestroy(EntryEvent<ID, T> event) throws CacheWriterException {
//doRepositoryOp(event.getOldValue(), FunctionUtils.toNullReturningFunction(getRepository()::delete));
doRepositoryOp(event.getKey(), FunctionUtils.toNullReturningFunction(getRepository()::deleteById));
}
@Override
public void beforeRegionClear(RegionEvent<ID, T> event) throws CacheWriterException {
if (isNukeAndPaveEnabled()) {
doRepositoryOp(null, FunctionUtils.toNullReturningFunction(it -> getRepository().deleteAll()));
}
}
@Override
public void beforeRegionDestroy(RegionEvent<ID, T> event) throws CacheWriterException {
// TODO: perhaps implement by releasing external data source resources
// (i.e. destroy database object(s), e.g. DROP TABLE)
}
@Override
protected CacheRuntimeException newCacheRuntimeException(Supplier<String> messageSupplier, Throwable cause) {
return new CacheWriterException(messageSupplier.get(), cause);
}
}

View File

@@ -0,0 +1,172 @@
/*
* Copyright 2017-present 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.geode.cache;
import java.util.function.Predicate;
import org.apache.geode.cache.CacheWriter;
import org.apache.geode.cache.Region;
import org.springframework.data.gemfire.PeerRegionFactoryBean;
import org.springframework.data.gemfire.client.ClientRegionFactoryBean;
import org.springframework.data.gemfire.config.annotation.RegionConfigurer;
import org.springframework.data.repository.CrudRepository;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Spring Data {@link RegionConfigurer} implementation used to adapt and register a Spring Data {@link CrudRepository}
* as a {@link CacheWriter} for a targeted {@link Region}.
*
* @author John Blum
* @param <T> {@link Class type} of the persistent entity.
* @param <ID> {@link Class type} of the persistent entity identifier (ID).
* @see java.util.function.Predicate
* @see org.apache.geode.cache.CacheWriter
* @see org.apache.geode.cache.Region
* @see org.springframework.data.gemfire.PeerRegionFactoryBean
* @see org.springframework.data.gemfire.client.ClientRegionFactoryBean
* @see org.springframework.data.gemfire.config.annotation.RegionConfigurer
* @see org.springframework.data.repository.CrudRepository
* @since 1.1.0
*/
public class RepositoryCacheWriterRegionConfigurer<T, ID> implements RegionConfigurer {
/**
* Factory method used to construct a new instance of {@link RepositoryCacheWriterRegionConfigurer} initialized with
* the given Spring Data {@link CrudRepository} used to write {@link Region} values to a backend data source
* /data store along with a given {@link Predicate} to identify/qualify the {@link Region} on which
* the {@link CrudRepository} will be registered and used as a {@link CacheWriter}.
*
* @param repository {@link CrudRepository} used to write {@link Region} values to a backend data source.
* @param regionBeanName {@link Predicate} used to identify/qualify the {@link Region} on which
* the {@link CrudRepository} will be registered and used as a {@link CacheWriter}.
* @return a new instance of {@link RepositoryCacheWriterRegionConfigurer}.
* @throws IllegalArgumentException if {@link CrudRepository} is {@literal null}.
* @see org.springframework.data.repository.CrudRepository
* @see java.util.function.Predicate
* @see #RepositoryCacheWriterRegionConfigurer(CrudRepository, Predicate)
*/
public static <T, ID> RepositoryCacheWriterRegionConfigurer<T, ID> create(@NonNull CrudRepository<T, ID> repository,
@Nullable Predicate<String> regionBeanName) {
return new RepositoryCacheWriterRegionConfigurer<>(repository, regionBeanName);
}
/**
* Factory method used to construct a new instance of {@link RepositoryCacheWriterRegionConfigurer} initialized with
* the given Spring Data {@link CrudRepository} used to write {@link Region} values to a backend data source
* /data store along with a given {@link Predicate} to identify/qualify the {@link Region} on which
* the {@link CrudRepository} will be registered and used as a {@link CacheWriter}.
*
* @param repository {@link CrudRepository} used to write {@link Region} values to a backend data source.
* @param regionBeanName {@link String} containing the bean name identifying/qualifying the {@link Region}
* on which the {@link CrudRepository} will be registered and used as a {@link CacheWriter}.
* @return a new instance of {@link RepositoryCacheWriterRegionConfigurer}.
* @throws IllegalArgumentException if {@link CrudRepository} is {@literal null}.
* @see org.springframework.data.repository.CrudRepository
* @see java.lang.String
* @see #create(CrudRepository, Predicate)
*/
public static <T, ID> RepositoryCacheWriterRegionConfigurer<T, ID> create(@NonNull CrudRepository<T, ID> repository,
@Nullable String regionBeanName) {
return create(repository, Predicate.isEqual(regionBeanName));
}
private final CrudRepository<T, ID> repository;
private final Predicate<String> regionBeanName;
/**
* Constructs a new instance of {@link RepositoryCacheWriterRegionConfigurer} initialized with the given Spring Data
* {@link CrudRepository} used to write {@link Region} values to a backend data source/data store along with
* a given {@link Predicate} to identify/qualify the {@link Region} on which the {@link CrudRepository} will be
* registered and used as a {@link CacheWriter}.
*
* @param repository {@link CrudRepository} used to write {@link Region} values to a backend data source.
* @param regionBeanName {@link Predicate} used to identify/qualify the {@link Region} on which
* the {@link CrudRepository} will be registered and used as a {@link CacheWriter}.
* @throws IllegalArgumentException if {@link CrudRepository} is {@literal null}.
* @see org.springframework.data.repository.CrudRepository
* @see java.util.function.Predicate
*/
public RepositoryCacheWriterRegionConfigurer(@NonNull CrudRepository<T, ID> repository,
@Nullable Predicate<String> regionBeanName) {
Assert.notNull(repository, "CrudRepository is required");
this.repository = repository;
this.regionBeanName = regionBeanName != null ? regionBeanName : beanName -> false;
}
/**
* Returns the configured {@link Predicate} used to identify/qualify the {@link Region}
* on which the {@link CrudRepository} will be registered as a {@link CacheWriter} for write through.
*
* @return the configured {@link Predicate} used to identify/qualify the {@link Region}
* targeted for the {@link CacheWriter} registration.
* @see java.util.function.Predicate
*/
protected @NonNull Predicate<String> getRegionBeanName() {
return regionBeanName;
}
/**
* Returns the configured Spring Data {@link CrudRepository} adapted/wrapped as a {@link CacheWriter}
* and used to write {@link Region} values to a backend data source/data store.
*
* @return the configured {@link CrudRepository} used to write {@link Region} values to a backend data source.
* @see org.springframework.data.repository.CrudRepository
*/
protected @NonNull CrudRepository<T, ID> getRepository() {
return this.repository;
}
@Override
@SuppressWarnings("unchecked")
public void configure(String beanName, ClientRegionFactoryBean<?, ?> bean) {
if (getRegionBeanName().test(beanName)) {
bean.setCacheWriter(newRepositoryCacheWriter());
}
}
@Override
@SuppressWarnings("unchecked")
public void configure(String beanName, PeerRegionFactoryBean<?, ?> bean) {
if (getRegionBeanName().test(beanName)) {
bean.setCacheWriter(newRepositoryCacheWriter());
}
}
/**
* Constructs a new instance of {@link RepositoryCacheWriter} adapting the {@link CrudRepository}
* as an instance of a {@link CacheWriter}.
*
* @return a new {@link RepositoryCacheWriter}.
* @see org.springframework.geode.cache.RepositoryCacheWriter
* @see org.springframework.data.repository.CrudRepository
* @see org.apache.geode.cache.CacheWriter
* @see #getRepository()
*/
@SuppressWarnings("rawtypes")
protected RepositoryCacheWriter newRepositoryCacheWriter() {
return new RepositoryCacheWriter<>(getRepository());
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2017-present 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.geode.cache.support;
import org.apache.geode.cache.CacheLoader;
/**
* The {@link CacheLoaderSupport} interface is an extension of {@link CacheLoader} and a {@link FunctionalInterface}
* useful in Lambda expressions.
*
* @author John Blum
* @see java.lang.FunctionalInterface
* @see org.apache.geode.cache.CacheLoader
* @since 1.0.0
*/
@FunctionalInterface
public interface CacheLoaderSupport<K, V> extends CacheLoader<K, V> {
/**
* Closes any resources opened and used by this {@link CacheLoader}.
*
* @see org.apache.geode.cache.CacheLoader#close()
*/
@Override
default void close() {}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2017-present 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.geode.cache.support;
import org.apache.geode.cache.CacheWriter;
import org.apache.geode.cache.CacheWriterException;
import org.apache.geode.cache.EntryEvent;
import org.apache.geode.cache.RegionEvent;
/**
* Class supporting the implementation of Apache Geode {@link CacheWriter CacheWriters}.
*
* @author John Blum
* @see org.apache.geode.cache.CacheWriter
* @since 1.1.0
*/
@SuppressWarnings("unused")
public interface CacheWriterSupport<K, V> extends CacheWriter<K, V> {
@Override
default void beforeCreate(EntryEvent<K, V> event) throws CacheWriterException { }
@Override
default void beforeUpdate(EntryEvent<K, V> event) throws CacheWriterException { }
@Override
default void beforeDestroy(EntryEvent<K, V> event) throws CacheWriterException { }
@Override
default void beforeRegionClear(RegionEvent<K, V> event) throws CacheWriterException { }
@Override
default void beforeRegionDestroy(RegionEvent<K, V> event) throws CacheWriterException { }
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2017-present 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.geode.cache.support;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import org.apache.geode.cache.CacheLoader;
import org.apache.geode.cache.CacheLoaderException;
import org.apache.geode.cache.CacheRuntimeException;
import org.apache.geode.cache.CacheWriter;
import org.apache.geode.cache.LoaderHelper;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.Repository;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Abstract base class supporting the implementation of Apache Geode {@link CacheLoader CacheLoaders}
* and {@link CacheWriter CacheWriters} backed by Spring Data {@link Repository Repositories}.
*
* @author John Blum
* @see org.apache.geode.cache.CacheLoader
* @see org.apache.geode.cache.CacheWriter
* @see org.springframework.context.EnvironmentAware
* @see org.springframework.core.env.Environment
* @see org.springframework.data.repository.CrudRepository
* @see org.springframework.data.repository.Repository
* @see org.springframework.geode.cache.support.CacheLoaderSupport
* @since 1.1.0
*/
public abstract class RepositoryCacheLoaderWriterSupport<T, ID>
implements CacheLoaderSupport<ID, T>, CacheWriterSupport<ID, T>, EnvironmentAware {
public static final String NUKE_AND_PAVE_PROPERTY = "spring.boot.data.gemfire.data.source.nuke-and-pave";
protected static final String DATA_ACCESS_ERROR =
"Exception occurred while accessing entity [%s] in external data source";
private final CrudRepository<T, ID> repository;
private Environment environment;
protected RepositoryCacheLoaderWriterSupport(@NonNull CrudRepository<T, ID> repository) {
Assert.notNull(repository, "Repository is required");
this.repository = repository;
}
protected boolean isNukeAndPaveEnabled() {
return getEnvironment()
.map(env -> env.getProperty(NUKE_AND_PAVE_PROPERTY, Boolean.class))
.orElse(Boolean.getBoolean(NUKE_AND_PAVE_PROPERTY));
}
@Override
public void setEnvironment(@Nullable Environment environment) {
this.environment = environment;
}
protected Optional<Environment> getEnvironment() {
return Optional.ofNullable(this.environment);
}
public @NonNull CrudRepository<T, ID> getRepository() {
return this.repository;
}
protected <S, R> R doRepositoryOp(S entity, Function<S, R> repositoryOperation) {
try {
return repositoryOperation.apply(entity);
}
catch (Throwable cause) {
throw newCacheRuntimeException(() -> String.format(DATA_ACCESS_ERROR, entity), cause);
}
}
@Override
public T load(LoaderHelper<ID, T> helper) throws CacheLoaderException {
return null;
}
protected abstract CacheRuntimeException newCacheRuntimeException(
Supplier<String> messageSupplier, Throwable cause);
@SuppressWarnings("unchecked")
public <U extends RepositoryCacheLoaderWriterSupport<T, ID>> U with(Environment environment) {
setEnvironment(environment);
return (U) this;
}
}

View File

@@ -0,0 +1,212 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import java.util.Set;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.data.gemfire.config.annotation.EnableClusterConfiguration;
import org.springframework.data.gemfire.support.ConnectionEndpointList;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.lang.NonNull;
import org.slf4j.Logger;
/**
* The {@link ClusterAvailableConfiguration} class is a Spring {@link Configuration} class that enables configuration
* when an Apache Geode cluster of servers are available.
*
* @author John Blum
* @see org.springframework.boot.autoconfigure.condition.AnyNestedCondition
* @see org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform
* @see org.springframework.boot.cloud.CloudPlatform
* @see org.springframework.context.annotation.ConditionContext
* @see org.springframework.context.annotation.Conditional
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.core.env.Environment
* @see org.springframework.core.type.AnnotatedTypeMetadata
* @see org.springframework.data.gemfire.config.annotation.EnableClusterConfiguration
* @see org.springframework.geode.config.annotation.ClusterAwareConfiguration
* @since 1.2.0
*/
@Configuration
@Conditional(ClusterAvailableConfiguration.AnyClusterAvailableCondition.class)
@EnableClusterConfiguration(requireHttps = false, useHttp = true)
@SuppressWarnings("unused")
public class ClusterAvailableConfiguration {
private static final Set<CloudPlatform> SUPPORTED_CLOUD_PLATFORMS =
CollectionUtils.asSet(CloudPlatform.CLOUD_FOUNDRY, CloudPlatform.KUBERNETES);
public static final class AnyClusterAvailableCondition extends AnyNestedCondition {
public AnyClusterAvailableCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
//@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
@Conditional(CloudFoundryClusterAvailableCondition.class)
static class IsCloudFoundryClusterAvailableCondition { }
//@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
@Conditional(KubernetesClusterAvailableCondition.class)
static class IsKubernetesClusterAvailableCondition { }
@Conditional(StandaloneClusterAvailableCondition.class)
static class IsStandaloneClusterAvailableCondition { }
}
protected static abstract class AbstractCloudPlatformAvailableCondition
extends ClusterAwareConfiguration.ClusterAwareCondition {
protected abstract String getCloudPlatformName();
@Override
protected String getRuntimeEnvironmentName() {
return getCloudPlatformName();
}
protected abstract boolean isCloudPlatformActive(@NonNull Environment environment);
protected boolean isInfoLoggingEnabled() {
return getLogger().isInfoEnabled();
}
protected boolean isMatchingStrictOrLoggable(boolean match, boolean strictMatch) {
return match && (strictMatch || isInfoLoggingEnabled());
}
@Override
public synchronized final boolean matches(@NonNull ConditionContext conditionContext,
@NonNull AnnotatedTypeMetadata typeMetadata) {
boolean match = isCloudPlatformActive(conditionContext.getEnvironment());
boolean strictMatch = isStrictMatch(conditionContext, typeMetadata);
if (isMatchingStrictOrLoggable(match, strictMatch)) {
match |= super.matches(conditionContext, typeMetadata);
}
if (match && !wasClusterAvailabilityEvaluated()) {
set(true);
}
return match;
}
@Override
protected void logConnectedRuntimeEnvironment(@NonNull Logger logger) {
if (logger.isInfoEnabled()) {
logger.info("Spring Boot application is running in a client/server topology,"
+ " inside a [{}] Cloud-managed Environment", getRuntimeEnvironmentName());
}
}
@Override
protected void logUnconnectedRuntimeEnvironment(@NonNull Logger logger) {
if (logger.isInfoEnabled()) {
logger.info("No cluster was found; Spring Boot application is running in a [{}]"
+ " Cloud-managed Environment", getRuntimeEnvironmentName());
}
}
@Override
protected void configureTopology(@NonNull Environment environment,
@NonNull ConnectionEndpointList connectionEndpoints, int connectionCount) {
// do nothing!
}
}
public static class CloudFoundryClusterAvailableCondition extends AbstractCloudPlatformAvailableCondition {
protected static final String CLOUD_FOUNDRY_NAME = "CloudFoundry";
protected static final String RUNTIME_ENVIRONMENT_NAME = "VMware Tanzu GemFire for VMs";
@Override
protected String getCloudPlatformName() {
return CLOUD_FOUNDRY_NAME;
}
@Override
protected String getRuntimeEnvironmentName() {
return RUNTIME_ENVIRONMENT_NAME;
}
@Override
protected boolean isCloudPlatformActive(@NonNull Environment environment) {
return environment != null && CloudPlatform.CLOUD_FOUNDRY.isActive(environment);
}
}
public static class KubernetesClusterAvailableCondition extends AbstractCloudPlatformAvailableCondition {
protected static final String KUBERNETES_NAME = "Kubernetes";
protected static final String RUNTIME_ENVIRONMENT_NAME = "VMware Tanzu GemFire for K8S";
@Override
protected String getCloudPlatformName() {
return KUBERNETES_NAME;
}
@Override
protected String getRuntimeEnvironmentName() {
return RUNTIME_ENVIRONMENT_NAME;
}
@Override
protected boolean isCloudPlatformActive(@NonNull Environment environment) {
return environment != null && CloudPlatform.KUBERNETES.isActive(environment);
}
}
public static class StandaloneClusterAvailableCondition
extends ClusterAwareConfiguration.ClusterAwareCondition {
@Override
public synchronized boolean matches(@NonNull ConditionContext conditionContext,
@NonNull AnnotatedTypeMetadata typeMetadata) {
return isNotSupportedCloudPlatform(conditionContext)
&& super.matches(conditionContext, typeMetadata);
}
private boolean isNotSupportedCloudPlatform(@NonNull ConditionContext conditionContext) {
return conditionContext != null && isNotSupportedCloudPlatform(conditionContext.getEnvironment());
}
private boolean isNotSupportedCloudPlatform(@NonNull Environment environment) {
CloudPlatform activeCloudPlatform = environment != null
? CloudPlatform.getActive(environment)
: null;
return !isSupportedCloudPlatform(activeCloudPlatform);
}
private boolean isSupportedCloudPlatform(@NonNull CloudPlatform cloudPlatform) {
return cloudPlatform != null && SUPPORTED_CLOUD_PLATFORMS.contains(cloudPlatform);
}
}
}

View File

@@ -0,0 +1,775 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.geode.cache.client.ClientCache;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.apache.geode.cache.client.Pool;
import org.apache.geode.cache.client.PoolManager;
import org.apache.geode.cache.server.CacheServer;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportAware;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport;
import org.springframework.data.gemfire.support.ConnectionEndpoint;
import org.springframework.data.gemfire.support.ConnectionEndpointList;
import org.springframework.data.gemfire.util.ArrayUtils;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.geode.cache.SimpleCacheResolver;
import org.springframework.geode.core.util.ObjectUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ClusterAwareConfiguration} class is a Spring {@link Configuration @Configuration} class imported by
* {@link EnableClusterAware} used to determine whether a Spring Boot application using Apache Geode should run
* in {@literal local-only mode} or {@literal client/server}.
*
* @author John Blum
* @see java.lang.annotation.Annotation
* @see java.net.InetSocketAddress
* @see java.net.Socket
* @see java.net.SocketAddress
* @see org.apache.geode.cache.client.ClientCache
* @see org.apache.geode.cache.client.ClientRegionShortcut
* @see org.apache.geode.cache.client.Pool
* @see org.apache.geode.cache.client.PoolManager
* @see org.apache.geode.cache.server.CacheServer
* @see org.springframework.context.ApplicationListener
* @see org.springframework.context.ConfigurableApplicationContext
* @see org.springframework.context.annotation.Condition
* @see org.springframework.context.annotation.ConditionContext
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.Import
* @see org.springframework.context.annotation.ImportAware
* @see org.springframework.context.event.ContextClosedEvent
* @see org.springframework.core.annotation.AnnotationAttributes
* @see org.springframework.core.env.ConfigurableEnvironment
* @see org.springframework.core.env.EnumerablePropertySource
* @see org.springframework.core.env.Environment
* @see org.springframework.core.env.PropertySource
* @see org.springframework.core.type.AnnotatedTypeMetadata
* @see org.springframework.core.type.AnnotationMetadata
* @see org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport
* @see org.springframework.data.gemfire.support.ConnectionEndpoint
* @see org.springframework.data.gemfire.support.ConnectionEndpointList
* @see org.springframework.geode.cache.SimpleCacheResolver
* @since 1.2.0
*/
@Configuration
@Import({ ClusterAvailableConfiguration.class, ClusterNotAvailableConfiguration.class })
public class ClusterAwareConfiguration extends AbstractAnnotationConfigSupport implements ImportAware {
static final boolean DEFAULT_CLUSTER_AWARE_CONDITION_MATCH = false;
static final boolean DEFAULT_CLUSTER_AWARE_CONDITION_STRICT_MATCH = false;
static final int DEFAULT_CACHE_SERVER_PORT = CacheServer.DEFAULT_PORT;
static final int DEFAULT_LOCATOR_PORT = 10334;
static final int DEFAULT_TIMEOUT_IN_MILLISECONDS = 500;
static final ClientRegionShortcut LOCAL_CLIENT_REGION_SHORTCUT = ClientRegionShortcut.LOCAL;
static final String LOCALHOST = "localhost";
static final String MATCHING_PROPERTY_PATTERN = "spring\\.data\\.gemfire\\.pool\\..*locators|servers";
static final String STRICT_MATCH_ATTRIBUTE_NAME = "strictMatch";
static final String CLUSTER_AWARE_CONFIGURATION_PROPERTY_SOURCE_NAME =
ClusterAwareConfiguration.class.getSimpleName().concat("PropertySource");
static final String SPRING_BOOT_DATA_GEMFIRE_CLUSTER_CONDITION_MATCH_PROPERTY =
"spring.boot.data.gemfire.cluster.condition.match";
static final String SPRING_BOOT_DATA_GEMFIRE_CLUSTER_CONDITION_MATCH_STRICT_PROPERTY =
"spring.boot.data.gemfire.cluster.condition.match.strict";
static final String SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY =
"spring.data.gemfire.cache.client.region.shortcut";
private static final AtomicBoolean strictMatchConfiguration =
new AtomicBoolean(DEFAULT_CLUSTER_AWARE_CONDITION_STRICT_MATCH);
private static final Function<ConditionContext, Boolean> configuredMatchFunction = conditionContext ->
Optional.ofNullable(conditionContext)
.map(ConditionContext::getEnvironment)
.map(environment -> environment.getProperty(SPRING_BOOT_DATA_GEMFIRE_CLUSTER_CONDITION_MATCH_PROPERTY,
Boolean.class, DEFAULT_CLUSTER_AWARE_CONDITION_MATCH))
.orElse(DEFAULT_CLUSTER_AWARE_CONDITION_MATCH);
private static final Logger logger = LoggerFactory.getLogger(ClusterAwareConfiguration.class);
/**
* @inheritDoc
*/
@Override
protected @NonNull Class<? extends Annotation> getAnnotationType() {
return EnableClusterAware.class;
}
protected boolean isStrictMatchConfigured(@NonNull AnnotationAttributes enableClusterAwareAttributes) {
return enableClusterAwareAttributes != null
&& Boolean.TRUE.equals(enableClusterAwareAttributes.getBoolean(STRICT_MATCH_ATTRIBUTE_NAME));
}
/**
* @inheritDoc
*/
@Override
public void setImportMetadata(@NonNull AnnotationMetadata importMetadata) {
if (isAnnotationPresent(importMetadata)) {
AnnotationAttributes enableClusterAwareAttributes = getAnnotationAttributes(importMetadata);
strictMatchConfiguration.set(isStrictMatchConfigured(enableClusterAwareAttributes));
}
}
@SuppressWarnings("unused")
public static class ClusterAwareCondition implements Condition {
private static final AtomicReference<Boolean> clusterAvailable = new AtomicReference<>(null);
protected static final String RUNTIME_ENVIRONMENT_NAME = "Apache Geode-based Cluster on Bare Metal";
private static @NonNull ApplicationListener<ContextClosedEvent> clusterAwareConditionResetOnContextClosedApplicationListener() {
return contextClosedEvent-> reset();
}
/**
* Determines whether an Apache Geode-based Cluster is available in the runtime environment.
*
* @return a boolean value indicating whether an Apache Geode-based Cluster is available in
* the runtime environment.
* @see #wasClusterAvailabilityEvaluated()
*/
public static boolean isAvailable() {
return Boolean.TRUE.equals(clusterAvailable.get());
}
/**
* Resets the state of this {@link Condition} to reevaluate whether an Apache Geode-based Cluster
* is available in the runtime environment.
*
* @see #set(Boolean)
*/
public static void reset() {
set(null);
}
/**
* Sets the state of the {@code clusterAvailable} variable.
*
* @param available state to set the {@code clusterAvailable} variable to.
* @see #reset()
*/
protected static void set(@Nullable Boolean available) {
clusterAvailable.set(available);
}
/**
* Determines whether the {@link Condition} that determines whether an Apache Geode-based Cluster is available
* in the runtime environment has been evaluated.
*
* @return a boolean value indicating whether the {@link Condition} that determines
* whether an Apache Geode-based Cluster is available in the runtime environment has been evaluated.
* @see #isAvailable()
*/
public static boolean wasClusterAvailabilityEvaluated() {
return clusterAvailable.get() != null;
}
/**
* Returns a {@link String} containing a description of the runtime environment.
*
* @return a {@link String} containing a description of the runtime environment.
*/
protected String getRuntimeEnvironmentName() {
return RUNTIME_ENVIRONMENT_NAME;
}
/**
* @inheritDoc
*/
@Override
public synchronized boolean matches(@NonNull ConditionContext conditionContext,
@NonNull AnnotatedTypeMetadata typeMetadata) {
boolean matches = isMatch(conditionContext) || doCachedMatch(conditionContext);
boolean strictMatch = isStrictMatch(conditionContext, typeMetadata);
failOnStrictMatchAndNoMatches(strictMatch, matches);
return matches;
}
boolean isMatch(@NonNull ConditionContext conditionContext) {
return isAvailable() || configuredMatchFunction.apply(conditionContext);
}
protected boolean isStrictMatch(@NonNull ConditionContext conditionContext,
@NonNull AnnotatedTypeMetadata typeMetadata) {
Environment environment = conditionContext.getEnvironment();
Function<ConfigurableListableBeanFactory, Boolean> isStrictMatchEnabledFunction = beanFactory -> {
boolean strictMatchEnabled = strictMatchConfiguration.get();
if (!strictMatchEnabled) {
String annotationName = EnableClusterAware.class.getName();
strictMatchEnabled = beanFactory != null
&& Arrays.stream(ArrayUtils.nullSafeArray(beanFactory.getBeanDefinitionNames(), String.class))
.map(beanFactory::getBeanDefinition)
.filter(AnnotatedBeanDefinition.class::isInstance)
.map(AnnotatedBeanDefinition.class::cast)
.map(AnnotatedBeanDefinition::getMetadata)
.filter(annotationMetadata -> annotationMetadata.hasAnnotation(annotationName))
.findFirst()
.map(annotationMetadata -> annotationMetadata.getAnnotationAttributes(annotationName))
.map(AnnotationAttributes::fromMap)
.map(annotationAttributes -> annotationAttributes.getBoolean(STRICT_MATCH_ATTRIBUTE_NAME))
.orElse(DEFAULT_CLUSTER_AWARE_CONDITION_STRICT_MATCH);
}
return strictMatchEnabled;
};
return environment.getProperty(SPRING_BOOT_DATA_GEMFIRE_CLUSTER_CONDITION_MATCH_STRICT_PROPERTY,
Boolean.class, isStrictMatchEnabledFunction.apply(conditionContext.getBeanFactory()));
}
protected boolean isStrictMatchAndNoMatches(boolean strictMatch, boolean matches) {
return strictMatch && !matches;
}
protected void failOnStrictMatchAndNoMatches(boolean strictMatch, boolean matches) {
if (isStrictMatchAndNoMatches(strictMatch, matches)) {
String message =
String.format("Failed to find available cluster in [%1$s] when strictMatch was [%2$s]",
getRuntimeEnvironmentName(), strictMatch);
throw new ClusterNotAvailableException(message);
}
}
/**
* Caches the result of the computed {@link #doMatch(ConditionContext)} operation.
*
* Subsequent calls returns the cached value of the computed (once) {@link #doMatch(ConditionContext)}
* operation.
*
* @param conditionContext Spring {@link ConditionContext} capturing the context in which the conditions
* are evaluated; must not be {@literal null}.
* @return a boolean value indicating whether the conditions match (i.e. {@literal true}).
* @see org.springframework.context.annotation.ConditionContext
* @see #registerApplicationListener(ConditionContext)
* @see #doMatch(ConditionContext)
*/
protected boolean doCachedMatch(@NonNull ConditionContext conditionContext) {
Supplier<Boolean> evaluateConditionMatches = () -> {
registerApplicationListener(conditionContext);
return doMatch(conditionContext);
};
UnaryOperator<Boolean> clusterAvailableUpdateFunction = currentClusterAvailable ->
ObjectUtils.initialize(currentClusterAvailable, evaluateConditionMatches);
return clusterAvailable.updateAndGet(clusterAvailableUpdateFunction);
}
protected @NonNull ConditionContext registerApplicationListener(@NonNull ConditionContext conditionContext) {
Optional.ofNullable(conditionContext)
.map(ConditionContext::getResourceLoader)
.filter(ConfigurableApplicationContext.class::isInstance)
.map(ConfigurableApplicationContext.class::cast)
.ifPresent(applicationContext -> applicationContext
.addApplicationListener(clusterAwareConditionResetOnContextClosedApplicationListener()));
return conditionContext;
}
/**
* Performs the actual conditional match to determine whether this Spring Boot for Apache Geode application
* can connect to an available Apache Geode cluster available in any environment (e.g. Standalone or Cloud).
*
* @param conditionContext Spring {@link ConditionContext} capturing the context in which the condition(s)
* are evaluated; must not be {@literal null}.
* @return the given {@link ConditionContext}.
* @see org.springframework.context.annotation.ConditionContext
* @see #doCachedMatch(ConditionContext)
* @see #getConnectionEndpoints(Environment)
* @see #countConnections(ConnectionEndpointList)
* @see #configureTopology(Environment, ConnectionEndpointList, int)
* @see #logRuntimeEnvironment(Logger, int)
* @see #isMatch(ConnectionEndpointList, int)
*/
protected boolean doMatch(@NonNull ConditionContext conditionContext) {
Environment environment = conditionContext.getEnvironment();
ConnectionEndpointList connectionEndpoints = getConnectionEndpoints(environment);
int connectionCount = countConnections(connectionEndpoints);
configureTopology(environment, connectionEndpoints, connectionCount);
logRuntimeEnvironment(getLogger(), connectionCount);
return isMatch(connectionEndpoints, connectionCount);
}
boolean isMatch(@NonNull ConnectionEndpointList connectionEndpoints, int connectionCount) {
return isConnected(connectionCount);
}
protected @NonNull Logger getLogger() {
return logger;
}
protected ConnectionEndpointList getConnectionEndpoints(@NonNull Environment environment) {
return new ConnectionEndpointList(getDefaultConnectionEndpoints(environment))
.add(getConfiguredConnectionEndpoints(environment))
.add(getPooledConnectionEndpoints(environment));
}
protected List<ConnectionEndpoint> getDefaultConnectionEndpoints(@NonNull Environment environment) {
return Arrays.asList(
new ConnectionEndpoint(LOCALHOST, DEFAULT_CACHE_SERVER_PORT),
new ConnectionEndpoint(LOCALHOST, DEFAULT_LOCATOR_PORT)
);
}
protected List<ConnectionEndpoint> getConfiguredConnectionEndpoints(@NonNull Environment environment) {
List<ConnectionEndpoint> connectionEndpoints = new ArrayList<>();
if (environment instanceof ConfigurableEnvironment) {
ConfigurableEnvironment configurableEnvironment = (ConfigurableEnvironment) environment;
MutablePropertySources propertySources = configurableEnvironment.getPropertySources();
if (propertySources != null) {
Pattern pattern = Pattern.compile(MATCHING_PROPERTY_PATTERN);
for (PropertySource<?> propertySource : propertySources) {
if (propertySource instanceof EnumerablePropertySource) {
EnumerablePropertySource<?> enumerablePropertySource =
(EnumerablePropertySource<?>) propertySource;
String[] propertyNames = enumerablePropertySource.getPropertyNames();
Arrays.stream(ArrayUtils.nullSafeArray(propertyNames, String.class))
.filter(StringUtils::hasText)
.filter(propertyName-> pattern.matcher(propertyName).find())
.forEach(propertyName -> {
String propertyValue = environment.getProperty(propertyName);
if (StringUtils.hasText(propertyValue)) {
int defaultPort = propertyName.toLowerCase().contains("servers")
? DEFAULT_CACHE_SERVER_PORT
: DEFAULT_LOCATOR_PORT;
String[] propertyValueArray = propertyValue.split(",");
ConnectionEndpointList list =
ConnectionEndpointList.parse(defaultPort, propertyValueArray);
connectionEndpoints.addAll(list);
}
});
}
}
}
}
return connectionEndpoints;
}
protected List<ConnectionEndpoint> getPooledConnectionEndpoints(@NonNull Environment environment) {
List<ConnectionEndpoint> pooledConnectionEndpoints = new ArrayList<>();
getPoolsFromApacheGeode().stream()
.filter(Objects::nonNull)
.map(ConnectionEndpointListBuilder::from)
.forEach(pooledConnectionEndpoints::addAll);
return pooledConnectionEndpoints;
}
protected Collection<Pool> getPoolsFromApacheGeode() {
Set<Pool> pools = new HashSet<>();
pools.addAll(getPoolsFromClientCache());
pools.addAll(getPoolsFromPoolManager());
return pools;
}
// Technically, should be registered with the PoolManager, but...
Collection<Pool> getPoolsFromClientCache() {
return SimpleCacheResolver.getInstance().resolveClientCache()
.map(ClientCache::getDefaultPool)
.map(Collections::singleton)
.orElseGet(Collections::emptySet);
}
Collection<Pool> getPoolsFromPoolManager() {
Map<String, Pool> namedPools = PoolManager.getAll();
return CollectionUtils.nullSafeMap(namedPools).values().stream()
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
protected int countConnections(@NonNull ConnectionEndpointList connectionEndpoints) {
int count = 0;
for (ConnectionEndpoint connectionEndpoint : connectionEndpoints) {
try (Socket socket = connect(connectionEndpoint)){
count += isConnected(socket) ? 1 : 0;
if (getLogger().isInfoEnabled()) {
getLogger().info("Successfully connected to {}", connectionEndpoint);
}
}
catch (IOException | SocketCreationException cause) {
if (getLogger().isInfoEnabled()) {
getLogger().info("Failed to connect to {}", connectionEndpoint);
}
if (getLogger().isDebugEnabled()) {
getLogger().debug("Connection failed because:", cause);
}
}
}
return count;
}
protected boolean isConnected(@NonNull Socket socket) {
return socket != null && socket.isConnected();
}
protected @NonNull Socket connect(@NonNull ConnectionEndpoint connectionEndpoint) throws IOException {
SocketAddress socketAddress = connectionEndpoint.toInetSocketAddress();
Socket socket = connectionEndpoint instanceof PoolConnectionEndpoint
? newSocket((PoolConnectionEndpoint) connectionEndpoint)
: newSocket(connectionEndpoint);
socket.connect(socketAddress, DEFAULT_TIMEOUT_IN_MILLISECONDS);
return socket;
}
protected @NonNull Socket newSocket(@NonNull ConnectionEndpoint connectionEndpoint) throws IOException {
Socket socket = new Socket();
socket.setKeepAlive(false);
socket.setReuseAddress(true);
socket.setSoLinger(false, 0);
return socket;
}
protected @NonNull Socket newSocket(@NonNull PoolConnectionEndpoint poolConnectionEndpoint) {
Function<Throwable, Socket> ioExceptionHandlingFunction = cause -> {
String message = String.format("Failed to create Socket from PoolConnectionEndpoint [%s]",
poolConnectionEndpoint);
throw new SocketCreationException(message, cause);
};
return poolConnectionEndpoint.getPool()
.map(Pool::getSocketFactory)
.map(socketFactory -> ObjectUtils.<Socket>doOperationSafely(socketFactory::createSocket,
ioExceptionHandlingFunction))
.orElseGet(() -> ObjectUtils.<Socket>doOperationSafely(() ->
newSocket((ConnectionEndpoint) poolConnectionEndpoint), ioExceptionHandlingFunction));
}
protected boolean close(@Nullable Socket socket) {
return ObjectUtils.<Boolean>doOperationSafely(() -> {
if (socket != null) {
socket.close();
return true;
}
return false;
}, cause -> false);
}
protected boolean isConnected(int connectionCount) {
return connectionCount > 0;
}
protected boolean isNotConnected(int connectionCount) {
return !isConnected(connectionCount);
}
private void configureEnvironment(@NonNull Environment environment) {
if (environment != null) {
if (!environment.containsProperty(SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY)) {
if (environment instanceof ConfigurableEnvironment) {
MutablePropertySources propertySources = ((ConfigurableEnvironment) environment).getPropertySources();
propertySources.addFirst(new MapPropertySource(CLUSTER_AWARE_CONFIGURATION_PROPERTY_SOURCE_NAME,
Collections.singletonMap(SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY,
LOCAL_CLIENT_REGION_SHORTCUT.name())));
}
else {
System.setProperty(SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY,
LOCAL_CLIENT_REGION_SHORTCUT.name());
}
}
}
}
protected void configureTopology(@NonNull Environment environment,
@NonNull ConnectionEndpointList connectionEndpoints, int connectionCount) {
if (isNotConnected(connectionCount)) {
configureEnvironment(environment);
}
}
protected void logConnectedRuntimeEnvironment(@NonNull Logger logger) {
if (logger.isInfoEnabled()) {
logger.info("Spring Boot application is running in a client/server topology"
+ " using a standalone Apache Geode-based cluster");
}
}
protected void logConnectedRuntimeEnvironment(@NonNull Logger logger, int connectionCount) {
if (logger.isInfoEnabled()) {
logger.info("Cluster was found; Auto-configuration made [{}] successful connection(s)",
connectionCount);
}
logConnectedRuntimeEnvironment(logger);
}
protected void logRuntimeEnvironment(@NonNull Logger logger, int connectionCount) {
if (isConnected(connectionCount)) {
logConnectedRuntimeEnvironment(logger, connectionCount);
}
else {
logUnconnectedRuntimeEnvironment(logger);
}
}
protected void logUnconnectedRuntimeEnvironment(@NonNull Logger logger) {
if (logger.isInfoEnabled()) {
logger.info("No cluster was found; Spring Boot application will run in standalone [LOCAL] mode"
+ " unless strictMode is false and the application is running in a Cloud-managed Environment");
}
}
}
protected static class ConnectionEndpointListBuilder {
protected static @NonNull ConnectionEndpointList from(@NonNull Pool pool) {
ConnectionEndpointList list = new ConnectionEndpointList();
if (pool != null) {
Set<InetSocketAddress> poolSocketAddresses = new HashSet<>();
collect(poolSocketAddresses, pool.getLocators());
collect(poolSocketAddresses, pool.getOnlineLocators());
collect(poolSocketAddresses, pool.getServers());
poolSocketAddresses.stream()
.map(ConnectionEndpoint::from)
.map(PoolConnectionEndpoint::from)
.map(it -> it.with(pool))
.forEach(list::add);
}
return list;
}
private static <T extends Collection<InetSocketAddress>> T collect(@NonNull T collection,
@NonNull Collection<InetSocketAddress> socketAddressesToCollect) {
CollectionUtils.nullSafeCollection(socketAddressesToCollect).stream()
.filter(Objects::nonNull)
.forEach(collection::add);
return collection;
}
}
protected static class PoolConnectionEndpoint extends ConnectionEndpoint {
protected static PoolConnectionEndpoint from(@NonNull ConnectionEndpoint connectionEndpoint) {
return new PoolConnectionEndpoint(connectionEndpoint.getHost(), connectionEndpoint.getPort());
}
private Pool pool;
PoolConnectionEndpoint(@NonNull String host, int port) {
super(host, port);
}
public Optional<Pool> getPool() {
return Optional.ofNullable(this.pool);
}
public @NonNull PoolConnectionEndpoint with(@Nullable Pool pool) {
this.pool = pool;
return this;
}
/**
* @inheritDoc
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof PoolConnectionEndpoint)) {
return false;
}
PoolConnectionEndpoint that = (PoolConnectionEndpoint) obj;
return super.equals(that)
&& this.getPool().equals(that.getPool());
}
/**
* @inheritDoc
*/
@Override
public int hashCode() {
int hashValue = super.hashCode();
hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(getPool());
return hashValue;
}
/**
* @inheritDoc
*/
@Override
public String toString() {
return String.format("ConnectionEndpoint [%1$s] from Pool [%2$s]",
super.toString(), getPool().map(Pool::getName).orElse(""));
}
}
@SuppressWarnings("unused")
protected static class SocketCreationException extends RuntimeException {
protected SocketCreationException() { }
protected SocketCreationException(String message) {
super(message);
}
protected SocketCreationException(Throwable cause) {
super(cause);
}
protected SocketCreationException(String message, Throwable cause) {
super(message, cause);
}
}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import static org.springframework.geode.config.annotation.ClusterAwareConfiguration.LOCAL_CLIENT_REGION_SHORTCUT;
import static org.springframework.geode.config.annotation.ClusterAwareConfiguration.SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.data.gemfire.GemfireUtils;
import org.springframework.data.gemfire.client.ClientRegionFactoryBean;
import org.springframework.data.gemfire.config.annotation.RegionConfigurer;
import org.springframework.data.gemfire.config.annotation.support.CacheTypeAwareRegionFactoryBean;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* The {@link ClusterNotAvailableConfiguration} class is a Spring {@link Configuration} class that enables configuration
* when an Apache Geode cluster of servers is not available.
*
* @author John Blum
* @see org.springframework.beans.factory.config.BeanPostProcessor
* @see org.springframework.boot.autoconfigure.condition.AllNestedConditions
* @see org.springframework.boot.cloud.CloudPlatform
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Condition
* @see org.springframework.context.annotation.ConditionContext
* @see org.springframework.context.annotation.Conditional
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.core.env.Environment
* @see org.springframework.core.type.AnnotatedTypeMetadata
* @see org.springframework.data.gemfire.client.ClientRegionFactoryBean
* @see org.springframework.data.gemfire.config.annotation.RegionConfigurer
* @see org.springframework.data.gemfire.config.annotation.support.CacheTypeAwareRegionFactoryBean
* @see org.springframework.geode.config.annotation.ClusterAwareConfiguration
* @since 1.2.0
*/
@Configuration
@Conditional(ClusterNotAvailableConfiguration.AllClusterNotAvailableConditions.class)
@SuppressWarnings("unused")
public class ClusterNotAvailableConfiguration {
@Bean
BeanPostProcessor localClientRegionBeanPostProcessor(@NonNull Environment environment) {
return new BeanPostProcessor() {
@Nullable @Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (isClientRegion(bean)) {
configureAsLocalClientRegion(environment, bean);
}
return bean;
}
};
}
@Bean
@SuppressWarnings("unused")
RegionConfigurer localClientRegionConfigurer(@NonNull Environment environment) {
return new RegionConfigurer() {
@Override
public void configure(String beanName, ClientRegionFactoryBean<?, ?> bean) {
configureAsLocalClientRegion(environment, bean);
}
};
}
protected boolean isClientRegion(@Nullable Object bean) {
return bean instanceof CacheTypeAwareRegionFactoryBean || bean instanceof ClientRegionFactoryBean;
}
protected @NonNull Object configureAsLocalClientRegion(@NonNull Environment environment,
@NonNull Object clientRegion) {
return clientRegion instanceof ClientRegionFactoryBean
? configureAsLocalClientRegion(environment, (ClientRegionFactoryBean<?, ?>) clientRegion)
: configureAsLocalClientRegion(environment, (CacheTypeAwareRegionFactoryBean<?, ?>) clientRegion);
}
protected @NonNull <K, V> CacheTypeAwareRegionFactoryBean<K, V> configureAsLocalClientRegion(
@NonNull Environment environment, @NonNull CacheTypeAwareRegionFactoryBean<K, V> clientRegion) {
ClientRegionShortcut shortcut =
environment.getProperty(SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY,
ClientRegionShortcut.class, LOCAL_CLIENT_REGION_SHORTCUT);
clientRegion.setClientRegionShortcut(shortcut);
clientRegion.setPoolName(GemfireUtils.DEFAULT_POOL_NAME);
return clientRegion;
}
protected @NonNull <K, V> ClientRegionFactoryBean<K, V> configureAsLocalClientRegion(
@NonNull Environment environment, @NonNull ClientRegionFactoryBean<K, V> clientRegion) {
ClientRegionShortcut shortcut =
environment.getProperty(SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY,
ClientRegionShortcut.class, LOCAL_CLIENT_REGION_SHORTCUT);
clientRegion.setPoolName(null);
clientRegion.setShortcut(shortcut);
return clientRegion;
}
public static final class AllClusterNotAvailableConditions extends AllNestedConditions {
public AllClusterNotAvailableConditions() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@Conditional(ClusterNotAvailableCondition.class)
static class IsClusterNotAvailableCondition { }
@Conditional(NotCloudFoundryEnvironmentCondition.class)
static class IsNotCloudFoundryEnvironmentCondition { }
@Conditional(NotKubernetesEnvironmentCondition.class)
static class IsNotKubernetesEnvironmentCondition { }
}
public static final class ClusterNotAvailableCondition extends ClusterAwareConfiguration.ClusterAwareCondition {
@Override
public synchronized boolean matches(@NonNull ConditionContext conditionContext,
@NonNull AnnotatedTypeMetadata typeMetadata) {
return !super.matches(conditionContext, typeMetadata);
}
}
public static final class NotCloudFoundryEnvironmentCondition implements Condition {
@Override
public boolean matches(@NonNull ConditionContext context, @NonNull AnnotatedTypeMetadata metadata) {
return !CloudPlatform.CLOUD_FOUNDRY.isActive(context.getEnvironment());
}
}
public static final class NotKubernetesEnvironmentCondition implements Condition {
@Override
public boolean matches(@NonNull ConditionContext context, @NonNull AnnotatedTypeMetadata metadata) {
return !CloudPlatform.KUBERNETES.isActive(context.getEnvironment());
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
/**
* The {@link ClusterNotAvailableException} is a {@link RuntimeException} indicating that no Apache Geode cluster
* was provisioned and available to service Apache Geode {@link org.apache.geode.cache.client.ClientCache} applications.
*
* @author John Blum
* @see java.lang.RuntimeException
* @see org.apache.geode.cache.client.ClientCache
* @since 1.0.0
*/
@SuppressWarnings("unused")
public class ClusterNotAvailableException extends RuntimeException {
/**
* Constructs a new uninitialized instance of {@link ClusterNotAvailableException}.
*/
public ClusterNotAvailableException() { }
/**
* Constructs a new instance of {@link ClusterNotAvailableException} initialized with
* the given {@link String message} describing the exception.
*
* @param message {@link String} containing a description of the exception.
*/
public ClusterNotAvailableException(String message) {
super(message);
}
/**
* Constructs a new instance of {@link ClusterNotAvailableException} initialized with
* the given {@link Throwable} as the cause of this exception.
*
* @param cause {@link Throwable} indicating the cause of this exception.
*/
public ClusterNotAvailableException(Throwable cause) {
super(cause);
}
/**
* Constructs a new instance of {@link ClusterNotAvailableException} initialized with
* the given {@link String message} describing the exception along with the given {@link Throwable}
* as the cause of this exception.
*
* @param message {@link String} containing a description of the exception.
* @param cause {@link Throwable} indicating the cause of this exception.
*/
public ClusterNotAvailableException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import java.lang.annotation.Annotation;
import java.util.Optional;
import org.apache.geode.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer;
import org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer;
import org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport;
import org.springframework.util.Assert;
/**
* The {@link DistributedSystemIdConfiguration} class is a Spring {@link Configuration} class used to configure
* the {@literal distributed-system-id} for a {@link Cache peer Cache member} in a cluster
* when using the P2P topology.
*
* @author John Blum
* @see org.apache.geode.cache.Cache
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.ImportAware
* @see org.springframework.core.annotation.AnnotationAttributes
* @see org.springframework.core.type.AnnotationMetadata
* @see org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer
* @see org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer
* @see org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport
* @see org.springframework.geode.config.annotation.UseDistributedSystemId
* @since 1.0.0
*/
@Configuration
@SuppressWarnings("unused")
public class DistributedSystemIdConfiguration extends AbstractAnnotationConfigSupport implements ImportAware {
private static final String GEMFIRE_DISTRIBUTED_SYSTEM_ID_PROPERTY = "distributed-system-id";
private Integer distributedSystemId;
private final Logger logger = LoggerFactory.getLogger(getClass());
@Override
protected Class<? extends Annotation> getAnnotationType() {
return UseDistributedSystemId.class;
}
@Override
@SuppressWarnings("all")
public void setImportMetadata(AnnotationMetadata importMetadata) {
if (isAnnotationPresent(importMetadata)) {
AnnotationAttributes distributedSystemIdAttributes = getAnnotationAttributes(importMetadata);
setDistributedSystemId(distributedSystemIdAttributes.containsKey("value")
? distributedSystemIdAttributes.getNumber("value") : null);
setDistributedSystemId(distributedSystemIdAttributes.containsKey("id")
? distributedSystemIdAttributes.getNumber("id") : null);
}
}
protected void setDistributedSystemId(Integer distributedSystemId) {
this.distributedSystemId = Optional.ofNullable(distributedSystemId)
.filter(id -> id > -1)
.orElse(this.distributedSystemId);
}
protected Optional<Integer> getDistributedSystemId() {
return Optional.ofNullable(this.distributedSystemId)
.filter(id -> id > -1);
}
protected Logger getLogger() {
return this.logger;
}
private int validateDistributedSystemId(int distributedSystemId) {
Assert.isTrue(distributedSystemId >= -1 && distributedSystemId < 256,
String.format("Distributed System ID [%d] must be between -1 and 255", distributedSystemId));
return distributedSystemId;
}
@Bean
ClientCacheConfigurer clientCacheDistributedSystemIdConfigurer() {
return (beanName, clientCacheFactoryBean) -> getDistributedSystemId().ifPresent(distributedSystemId -> {
Logger logger = getLogger();
if (logger.isWarnEnabled()) {
logger.warn("Distributed System Id [{}] was set on the ClientCache instance, which will not have any effect",
distributedSystemId);
}
});
}
@Bean
PeerCacheConfigurer peerCacheDistributedSystemIdConfigurer() {
return (beanName, cacheFactoryBean) ->
getDistributedSystemId().ifPresent(id -> cacheFactoryBean.getProperties()
.setProperty(GEMFIRE_DISTRIBUTED_SYSTEM_ID_PROPERTY,
String.valueOf(validateDistributedSystemId(id))));
}
}

View File

@@ -0,0 +1,159 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import java.lang.annotation.Annotation;
import java.util.Optional;
import org.apache.geode.cache.client.ClientCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer;
import org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer;
import org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport;
import org.springframework.util.StringUtils;
/**
* The {@link DurableClientConfiguration} class is a Spring {@link Configuration} class used to configure
* this {@link ClientCache} instance as a {@literal Durable Client} by setting the {@literal durable-client-id}
* and {@literal durable-client-timeout} properties in addition to enabling {@literal keepAlive}
* on {@link ClientCache} shutdown.
*
* @author John Blum
* @see org.apache.geode.cache.client.ClientCache
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.ImportAware
* @see org.springframework.core.annotation.AnnotationAttributes
* @see org.springframework.core.type.AnnotationMetadata
* @see org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer
* @see org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer
* @see org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport
* @see org.springframework.geode.config.annotation.EnableDurableClient
* @since 1.0.0
*/
@Configuration
@SuppressWarnings("unused")
public class DurableClientConfiguration extends AbstractAnnotationConfigSupport implements ImportAware {
public static final boolean DEFAULT_KEEP_ALIVE = true;
public static final boolean DEFAULT_READY_FOR_EVENTS = true;
public static final int DEFAULT_DURABLE_CLIENT_TIMEOUT = 300;
private Boolean keepAlive = DEFAULT_KEEP_ALIVE;
private Boolean readyForEvents = DEFAULT_READY_FOR_EVENTS;
private Integer durableClientTimeout = DEFAULT_DURABLE_CLIENT_TIMEOUT;
private final Logger logger = LoggerFactory.getLogger(getClass());
private String durableClientId;
@Override
protected Class<? extends Annotation> getAnnotationType() {
return EnableDurableClient.class;
}
@Override
@SuppressWarnings("all")
public void setImportMetadata(AnnotationMetadata importMetadata) {
if (isAnnotationPresent(importMetadata)) {
AnnotationAttributes enableDurableClientAttributes = getAnnotationAttributes(importMetadata);
this.durableClientId = enableDurableClientAttributes.containsKey("id")
? enableDurableClientAttributes.getString("id")
: null;
this.durableClientTimeout = enableDurableClientAttributes.containsKey("timeout")
? enableDurableClientAttributes.getNumber("timeout")
: DEFAULT_DURABLE_CLIENT_TIMEOUT;
this.keepAlive = enableDurableClientAttributes.containsKey("keepAlive")
? enableDurableClientAttributes.getBoolean("keepAlive")
: DEFAULT_KEEP_ALIVE;
this.readyForEvents = enableDurableClientAttributes.containsKey("readyForEvents")
? enableDurableClientAttributes.getBoolean("readyForEvents")
: DEFAULT_READY_FOR_EVENTS;
}
}
protected Optional<String> getDurableClientId() {
return Optional.ofNullable(this.durableClientId)
.filter(StringUtils::hasText);
}
protected Integer getDurableClientTimeout() {
return this.durableClientTimeout != null
? this.durableClientTimeout
: DEFAULT_DURABLE_CLIENT_TIMEOUT;
}
protected Boolean getKeepAlive() {
return this.keepAlive != null
? this.keepAlive
: DEFAULT_KEEP_ALIVE;
}
protected Boolean getReadyForEvents() {
return this.readyForEvents != null
? this.readyForEvents
: DEFAULT_READY_FOR_EVENTS;
}
protected Logger getLogger() {
return this.logger;
}
@Bean
ClientCacheConfigurer clientCacheDurableClientConfigurer() {
return (beanName, clientCacheFactoryBean) -> getDurableClientId().ifPresent(durableClientId -> {
clientCacheFactoryBean.setDurableClientId(durableClientId);
clientCacheFactoryBean.setDurableClientTimeout(getDurableClientTimeout());
clientCacheFactoryBean.setKeepAlive(getKeepAlive());
clientCacheFactoryBean.setReadyForEvents(getReadyForEvents());
});
}
@Bean
PeerCacheConfigurer peerCacheDurableClientConfigurer() {
return (beanName, cacheFactoryBean) -> getDurableClientId().ifPresent(durableClientId -> {
Logger logger = getLogger();
if (logger.isWarnEnabled()) {
logger.warn("Durable Client ID [{}] was set on a peer Cache instance, which will not have any effect",
durableClientId);
}
});
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.apache.geode.cache.client.ClientCache;
import org.springframework.context.annotation.Import;
/**
* The {@link EnableClusterAware} annotation helps Spring Boot applications using Apache Geode decide whether it needs
* to operate in {@literal local-only mode} or in a {@literal client/server topology}.
*
* @author John Blum
* @see java.lang.annotation.Annotation
* @see java.lang.annotation.Documented
* @see java.lang.annotation.Inherited
* @see java.lang.annotation.Retention
* @see java.lang.annotation.Target
* @see org.apache.geode.cache.client.ClientCache
* @see org.springframework.context.annotation.Import
* @see org.springframework.data.gemfire.config.annotation.EnableClusterConfiguration
* @since 1.2.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Import(ClusterAwareConfiguration.class)
@SuppressWarnings("unused")
public @interface EnableClusterAware {
/**
* Determines whether the matching algorithm is strict.
*
* This means that at least 1 connection to a cluster of servers (1 or more) must be established
* before the cluster aware logic considers that a cluster actually exists.
*
* Previously, in cloud-managed environments (e.g. VMware Tanzu Application Service (TAS) for VMs, formerly known as
* Pivotal Platform or Pivotal CloudFoundry (PCF), or Kubernetes, known as VMware Tanzu Application Service for K8S)
* it was assumed that a cluster would be provisioned and available, and that the Spring Boot, Apache Geode
* {@link ClientCache} application would connect to the cluster on deployment (push).
*
* However, is entirely possible that users may push Spring Boot, Apache Geode {@link ClientCache} applications
* to a cloud-managed environment where not cluster was provisioned and is available, and user simply want their
* apps to run in local-only mode.
*
* The strict match configuration setting absolutely requires that at least 1 connection must be established. Use
* of this configuration setting also promotes a fail-fast protocol, or at least early detection (when log levels
* are adjusted accordingly) that a cluster is not available.
*
* Use {@literal spring.boot.data.gemfire.cluster.condition.match.strict}
* in Spring Boot {@literal application.properties}.
*
* Defaults to {@literal false}.
*
* @return a boolean value indicating whether strict matching mode is enabled.
*/
boolean strictMatch() default ClusterAwareConfiguration.DEFAULT_CLUSTER_AWARE_CONDITION_STRICT_MATCH;
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.apache.geode.cache.client.ClientCache;
import org.springframework.context.annotation.Import;
/**
* The {@link EnableDurableClient} annotation configures a {@link ClientCache} instance as a {@literal Durable Client}.
*
* @author John Blum
* @see java.lang.annotation.Documented
* @see java.lang.annotation.Inherited
* @see java.lang.annotation.Retention
* @see java.lang.annotation.Target
* @see org.apache.geode.cache.client.ClientCache
* @see org.springframework.context.annotation.Import
* @see org.springframework.geode.config.annotation.DurableClientConfiguration
* @since 1.0.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Import(DurableClientConfiguration.class)
@SuppressWarnings("unused")
public @interface EnableDurableClient {
/**
* Used only for clients in a client/server installation. If set, this indicates that the client is durable
* and identifies the client. The ID is used by servers to reestablish any messaging that was interrupted
* by client downtime.
*/
String id();
/**
* Configure whether the server should keep the durable client's queues alive for the timeout period.
*
* Defaults to {@literal true}.
*/
boolean keepAlive() default DurableClientConfiguration.DEFAULT_KEEP_ALIVE;
/**
* Configures whether the {@link ClientCache} is ready to recieve events on startup.
*
* Defaults to {@literal true}.
*/
boolean readyForEvents() default DurableClientConfiguration.DEFAULT_READY_FOR_EVENTS;
/**
* Used only for clients in a client/server installation. Number of seconds this client can remain disconnected
* from its server and have the server continue to accumulate durable events for it.
*
* Defaults to {@literal 300 seconds}, or {@literal 5 minutes}.
*/
int timeout() default DurableClientConfiguration.DEFAULT_DURABLE_CLIENT_TIMEOUT;
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
/**
* Spring {@link Annotation} to enable Apache Geode Security (Auth).
*
* @author John Blum
* @see java.lang.annotation.Annotation
* @see java.lang.annotation.Documented
* @see java.lang.annotation.Inherited
* @see java.lang.annotation.Retention
* @see java.lang.annotation.Target
* @see org.springframework.context.annotation.Import
* @see org.springframework.geode.config.annotation.SecurityManagerConfiguration
* @since 1.1.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Import(SecurityManagerConfiguration.class)
@SuppressWarnings("unused")
public @interface EnableSecurityManager {
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
/**
* Spring {@link Annotation} to enable Apache Geode Security (Authentication/Authorization (Auth)) through proxying.
*
* @author John Blum
* @see java.lang.annotation.Annotation
* @see java.lang.annotation.Documented
* @see java.lang.annotation.Inherited
* @see java.lang.annotation.Retention
* @see java.lang.annotation.Target
* @see org.springframework.context.annotation.Import
* @see org.springframework.geode.config.annotation.SecurityManagerProxyConfiguration
* @since 1.1.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Import(SecurityManagerProxyConfiguration.class)
@SuppressWarnings("unused")
public @interface EnableSecurityManagerProxy {
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import java.lang.annotation.Annotation;
import java.util.Optional;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.client.ClientCache;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.gemfire.CacheFactoryBean;
import org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer;
import org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer;
import org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport;
import org.springframework.util.StringUtils;
/**
* The {@link GroupsConfiguration} class is a Spring {@link Configuration} class used to configure the {@literal groups}
* in which is member belongs in an Apache Geode distributed system, whether the member is a {@link ClientCache} in a
* client/server topology or a {@link Cache peer Cache} in a cluster using the P2P topology.
*
* @author John Blum
* @see org.apache.geode.cache.Cache
* @see org.apache.geode.cache.client.ClientCache
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.ImportAware
* @see org.springframework.core.annotation.AnnotationAttributes
* @see org.springframework.core.type.AnnotationMetadata
* @see org.springframework.data.gemfire.CacheFactoryBean
* @see org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer
* @see org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer
* @see org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport
* @see org.springframework.geode.config.annotation.UseGroups
* @since 1.0.0
*/
@Configuration
@SuppressWarnings("unused")
public class GroupsConfiguration extends AbstractAnnotationConfigSupport implements ImportAware {
private static final String GEMFIRE_GROUPS_PROPERTY = "groups";
private String[] groups = {};
@Override
protected Class<? extends Annotation> getAnnotationType() {
return UseGroups.class;
}
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
if (isAnnotationPresent(importMetadata)) {
AnnotationAttributes inGroupsAttributes = getAnnotationAttributes(importMetadata);
setGroups(inGroupsAttributes.containsKey("value")
? inGroupsAttributes.getStringArray("value") : null);
setGroups(inGroupsAttributes.containsKey("groups")
? inGroupsAttributes.getStringArray("groups") : null);
}
}
protected void setGroups(String[] groups) {
this.groups = Optional.ofNullable(groups)
.filter(it -> it.length > 0)
.orElse(this.groups);
}
protected Optional<String[]> getGroups() {
return Optional.ofNullable(this.groups)
.filter(it -> it.length > 0);
}
@Bean
ClientCacheConfigurer clientCacheGroupsConfigurer() {
return (beaName, clientCacheFactoryBean) -> configureGroups(clientCacheFactoryBean);
}
@Bean
PeerCacheConfigurer peerCacheGroupsConfigurer() {
return (beaName, peerCacheFactoryBean) -> configureGroups(peerCacheFactoryBean);
}
private void configureGroups(CacheFactoryBean cacheFactoryBean) {
getGroups().ifPresent(groups -> cacheFactoryBean.getProperties()
.setProperty(GEMFIRE_GROUPS_PROPERTY, StringUtils.arrayToCommaDelimitedString(groups)));
}
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import java.lang.annotation.Annotation;
import java.util.Optional;
import java.util.Properties;
import org.apache.geode.cache.Cache;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer;
import org.springframework.data.gemfire.config.annotation.LocatorConfigurer;
import org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer;
import org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport;
import org.springframework.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link LocatorsConfiguration} class is a Spring {@link Configuration} class used to configure Apache Geode
* {@literal locators} and/or {@literal remote-locators} properties used by a {@link Cache peer Cache member}
* to join a cluster of servers when using the P2P topology.
*
* The {@literal remote-locators} property is used to configure the Locators that a cluster will use in order to
* connect to a remote site in a multi-site (WAN) topology configuration. To use Locators in a WAN configuration,
* you must specify a unique distributed system ID ({@literal distributed-system-id}) for the local cluster
* and remote Locator(s) for the remote clusters to which you will connect.
*
* @author John Blum
* @see org.apache.geode.cache.Cache
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.ImportAware
* @see org.springframework.core.annotation.AnnotationAttributes
* @see org.springframework.core.type.AnnotationMetadata
* @see org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer
* @see org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport
* @see org.springframework.geode.config.annotation.UseLocators
* @since 1.0.0
*/
@Configuration
@SuppressWarnings("unused")
public class LocatorsConfiguration extends AbstractAnnotationConfigSupport implements ImportAware {
protected static final String DEFAULT_LOCATORS = "localhost[10334]";
protected static final String DEFAULT_REMOTE_LOCATORS = "";
protected static final String LOCATORS_PROPERTY = "locators";
protected static final String REMOTE_LOCATORS_PROPERTY = "remote-locators";
private final Logger logger = LoggerFactory.getLogger(getClass());
private String locators;
private String remoteLocators;
@Override
protected Class<? extends Annotation> getAnnotationType() {
return UseLocators.class;
}
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
if (isAnnotationPresent(importMetadata)) {
AnnotationAttributes useLocatorsAttributes = getAnnotationAttributes(importMetadata);
setLocators(useLocatorsAttributes.containsKey("locators")
? useLocatorsAttributes.getString("locators") : null);
setRemoteLocators(useLocatorsAttributes.containsKey("remoteLocators")
? useLocatorsAttributes.getString("remoteLocators") : null);
}
}
protected void setLocators(String locators) {
this.locators = StringUtils.hasText(locators) ? locators : null;
}
protected Optional<String> getLocators() {
return Optional.ofNullable(this.locators)
.filter(StringUtils::hasText);
}
protected Logger getLogger() {
return this.logger;
}
protected void setRemoteLocators(String remoteLocators) {
this.remoteLocators = StringUtils.hasText(remoteLocators) ? remoteLocators : null;
}
protected Optional<String> getRemoteLocators() {
return Optional.ofNullable(this.remoteLocators)
.filter(StringUtils::hasText);
}
@Bean
ClientCacheConfigurer clientCacheLocatorsConfigurer() {
return (beanName, clientCacheFactoryBean) -> {
Logger logger = getLogger();
getLocators().ifPresent(locators -> {
if (logger.isWarnEnabled()) {
logger.warn("The '{}' property was configured [{}];"
+ " however, this value does not have any effect for ClientCache instances",
LOCATORS_PROPERTY, locators);
}
});
getRemoteLocators().ifPresent(remoteLocators -> {
if (logger.isWarnEnabled()) {
logger.warn("The '{}' property was configured [{}];"
+ " however, this value does not have any effect for ClientCache instances",
REMOTE_LOCATORS_PROPERTY, remoteLocators);
}
});
};
}
@Bean
LocatorConfigurer locatorLocatorsConfigurer() {
return (beanName, locatorFactoryBean) -> {
Properties gemfireProperties = locatorFactoryBean.getGemFireProperties();
getLocators().ifPresent(locators -> gemfireProperties.setProperty(LOCATORS_PROPERTY, locators));
getRemoteLocators().ifPresent(remoteLocators ->
gemfireProperties.setProperty(REMOTE_LOCATORS_PROPERTY, remoteLocators));
};
}
@Bean
PeerCacheConfigurer peerCacheLocatorsConfigurer() {
return (beanName, cacheFactoryBean) -> {
Properties gemfireProperties = cacheFactoryBean.getProperties();
getLocators().ifPresent(locators -> gemfireProperties.setProperty(LOCATORS_PROPERTY, locators));
getRemoteLocators().ifPresent(remoteLocators ->
gemfireProperties.setProperty(REMOTE_LOCATORS_PROPERTY, remoteLocators));
};
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import java.lang.annotation.Annotation;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.client.ClientCache;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.gemfire.CacheFactoryBean;
import org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer;
import org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer;
import org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport;
import org.springframework.util.StringUtils;
/**
* The {@link MemberNameConfiguration} class is a Spring {@link Configuration} class used to configure an Apache Geode
* member name in the distributed system, whether the member is a {@link ClientCache client} in the client/server
* topology or a {@link Cache peer} in a cluster using the P2P topology.
*
* @author John Blum
* @see org.apache.geode.cache.Cache
* @see org.apache.geode.cache.client.ClientCache
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.ImportAware
* @see org.springframework.core.annotation.AnnotationAttributes
* @see org.springframework.core.type.AnnotationMetadata
* @see org.springframework.data.gemfire.CacheFactoryBean
* @see org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer
* @see org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer
* @see org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport
* @see org.springframework.geode.config.annotation.UseMemberName
* @since 1.0.0
*/
@Configuration
@SuppressWarnings("unused")
public class MemberNameConfiguration extends AbstractAnnotationConfigSupport implements ImportAware {
private static final String GEMFIRE_NAME_PROPERTY = "name";
private static final String SPRING_APPLICATION_NAME_PROPERTY = "spring.application.name";
private static final String SPRING_DATA_GEMFIRE_CACHE_NAME_PROPERTY = "spring.data.gemfire.cache.name";
private static final String SPRING_DATA_GEMFIRE_NAME_PROPERTY = "spring.data.gemfire.name";
private static final String SPRING_DATA_GEODE_CACHE_NAME_PROPERTY = "spring.data.geode.cache.name";
private static final String SPRING_DATA_GEODE_NAME_PROPERTY = "spring.data.geode.name";
private static final Set<String> NAME_PROPERTIES = new HashSet<>();
static {
NAME_PROPERTIES.add(SPRING_APPLICATION_NAME_PROPERTY);
NAME_PROPERTIES.add(SPRING_DATA_GEMFIRE_CACHE_NAME_PROPERTY);
NAME_PROPERTIES.add(SPRING_DATA_GEMFIRE_NAME_PROPERTY);
NAME_PROPERTIES.add(SPRING_DATA_GEODE_CACHE_NAME_PROPERTY);
NAME_PROPERTIES.add(SPRING_DATA_GEODE_NAME_PROPERTY);
}
private String memberName;
@Override
protected Class<? extends Annotation> getAnnotationType() {
return UseMemberName.class;
}
@Override
@SuppressWarnings("all")
public void setImportMetadata(AnnotationMetadata importMetadata) {
if (isAnnotationPresent(importMetadata)) {
AnnotationAttributes memberNameAttributes = getAnnotationAttributes(importMetadata);
setMemberName(memberNameAttributes.containsKey("value")
? memberNameAttributes.getString("value") : null);
setMemberName(memberNameAttributes.containsKey("name")
? memberNameAttributes.getString("name") : null);
}
}
protected void setMemberName(String memberName) {
this.memberName = Optional.ofNullable(memberName)
.filter(StringUtils::hasText)
.orElse(this.memberName);
}
protected Optional<String> getMemberName() {
return Optional.ofNullable(this.memberName)
.filter(StringUtils::hasText);
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) // apply first (e.g. before CacheNameAutoConfiguration)
ClientCacheConfigurer clientCacheMemberNameConfigurer(Environment environment) {
return (beanName, clientCacheFactoryBean) -> configureMemberName(environment, clientCacheFactoryBean);
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) // apply first (e.g. before CacheNameAutoConfiguration)
PeerCacheConfigurer peerCacheMemberNameConfigurer(Environment environment) {
return (beanName, peerCacheFactoryBean) -> configureMemberName(environment, peerCacheFactoryBean);
}
private void configureMemberName(Environment environment, CacheFactoryBean cacheFactoryBean) {
getMemberName()
.filter(memberName -> namePropertiesNotPresent(environment))
.ifPresent(memberName ->
cacheFactoryBean.getProperties().setProperty(GEMFIRE_NAME_PROPERTY, memberName));
}
private boolean namePropertiesArePresent(Environment environment) {
return NAME_PROPERTIES.stream()
.anyMatch(environment::containsProperty);
}
private boolean namePropertiesNotPresent(Environment environment) {
return !namePropertiesArePresent(environment);
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer;
import org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer;
/**
* Spring {@link Configuration} class used to configure a {@link org.apache.geode.security.SecurityManager},
* thereby enabling Security (Auth) on this Apache Geode node.
*
* @author John Blum
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer
* @see org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer
* @since 1.1.0
*/
@Configuration
@SuppressWarnings("unused")
public class SecurityManagerConfiguration {
@Bean
ClientCacheConfigurer clientSecurityManagerConfigurer(org.apache.geode.security.SecurityManager securityManager) {
return (beanName, clientCacheFactoryBean) -> clientCacheFactoryBean.setSecurityManager(securityManager);
}
@Bean
PeerCacheConfigurer peerSecurityManagerConfigurer(org.apache.geode.security.SecurityManager securityManager) {
return (beanName, cacheFactoryBean) -> cacheFactoryBean.setSecurityManager(securityManager);
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import java.util.Properties;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.data.gemfire.config.annotation.EnableBeanFactoryLocator;
import org.springframework.data.gemfire.config.annotation.EnableSecurity;
import org.springframework.geode.security.support.SecurityManagerProxy;
/**
* Spring {@link Configuration} class used to configure a {@link org.apache.geode.security.SecurityManager},
* thereby enabling Security (Auth) on this Apache Geode node.
*
* @author John Blum
* @see org.springframework.context.ApplicationListener
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.event.ContextRefreshedEvent
* @see org.springframework.data.gemfire.config.annotation.EnableBeanFactoryLocator
* @see org.springframework.data.gemfire.config.annotation.EnableSecurity
* @see org.springframework.geode.security.support.SecurityManagerProxy
* @since 1.1.0
*/
@Configuration
@EnableBeanFactoryLocator
@EnableSecurity(securityManagerClassName = "org.springframework.geode.security.support.SecurityManagerProxy")
@SuppressWarnings("unused")
public class SecurityManagerProxyConfiguration implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
SecurityManagerProxy securityManagerProxy = SecurityManagerProxy.getInstance();
securityManagerProxy.setBeanFactory(event.getApplicationContext().getAutowireCapableBeanFactory());
securityManagerProxy.init(new Properties());
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.client.ClientCache;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AliasFor;
/**
* The {@link UseDistributedSystemId} annotation configures the {@literal distributed-system-id} property
* of a {@link Cache peer Cache member} in an Apache Geode P2P topology.
*
* This configuration annotation is only applicable on {@link Cache peer Cache members}
* and has no effect on {@link ClientCache} instances.
*
* @author John Blum
* @see java.lang.annotation.Documented
* @see java.lang.annotation.Inherited
* @see java.lang.annotation.Retention
* @see java.lang.annotation.Target
* @see org.apache.geode.cache.Cache
* @see org.springframework.context.annotation.Import
* @see org.springframework.core.annotation.AliasFor
* @see org.springframework.geode.config.annotation.DistributedSystemIdConfiguration
* @since 1.0.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Import(DistributedSystemIdConfiguration.class)
@SuppressWarnings("unused")
public @interface UseDistributedSystemId {
/**
* Configures the identifier used to distinguish messages from different distributed systems.
*
* This is required for Portable Data eXchange (PDX) data serialization.
*
* Set distributed-system-id to different values for different systems in a multi-site (WAN) configuration,
* and to different values for production vs. development environments. This setting must be the same
* for every member of a given distributed system and unique to each distributed system within a WAN installation.
*
* Valid values are integers in the range -1…255. -1 means no setting.
*
* Defaults to {@literal -1}.
*/
@AliasFor("id")
int value() default -1;
/**
* Configures the identifier used to distinguish messages from different distributed systems.
*
* Alias for {@literal #value()}.
*/
@AliasFor("value")
int id() default -1;
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.client.ClientCache;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AliasFor;
/**
* The {@link UseGroups} annotation configures the groups in which the member belongs in an Apache Geode
* distributed system, whether the member is a {@link ClientCache} in a client/server topology
* or a {@link Cache peer Cache} in a cluster using the P2P topology.
*
* @author John Blum
* @see java.lang.annotation.Documented
* @see java.lang.annotation.Inherited
* @see java.lang.annotation.Retention
* @see java.lang.annotation.Target
* @see org.apache.geode.cache.Cache
* @see org.apache.geode.cache.client.ClientCache
* @see org.springframework.context.annotation.Import
* @see org.springframework.core.annotation.AliasFor
* @see org.springframework.geode.config.annotation.GroupsConfiguration
* @since 1.0.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Import(GroupsConfiguration.class)
@SuppressWarnings("unused")
public @interface UseGroups {
@AliasFor("groups")
String[] value() default {};
@AliasFor("value")
String[] groups() default {};
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import static org.springframework.geode.config.annotation.LocatorsConfiguration.DEFAULT_LOCATORS;
import static org.springframework.geode.config.annotation.LocatorsConfiguration.DEFAULT_REMOTE_LOCATORS;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.apache.geode.cache.Cache;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AliasFor;
/**
* The {@link UseLocators} annotation configures the {@literal locators} and/or {@literal remote-locators} Apache Geode
* properties used by a {@link Cache peer Cache member} to join a cluster of servers when using the P2P topology
* as well as when configuring the multi-site, WAN topology.
*
* @author John Blum
* @see java.lang.annotation.Documented
* @see java.lang.annotation.Inherited
* @see java.lang.annotation.Retention
* @see java.lang.annotation.Target
* @see org.apache.geode.cache.Cache
* @see org.springframework.context.annotation.Import
* @see org.springframework.core.annotation.AliasFor
* @see org.springframework.geode.config.annotation.MemberNameConfiguration
* @since 1.0.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Import(LocatorsConfiguration.class)
@SuppressWarnings("unused")
public @interface UseLocators {
/**
* @see #locators()
*/
@AliasFor("locators")
String value() default DEFAULT_LOCATORS;
/**
* The list of Locators used by system members. The list must be configured consistently for every member of
* the cluster (a.k.a. distributed system). If the list is empty, Locators will not be used.
*
* For each Locator, provide a hostname and/or address (separated by @, if you use both), followed by
* a port number in brackets.
*
* For example:
*
* <code>
* locators=address1[port1],address2[port2],...,addressN[portN]
* locators=hostname1@address1[port1],hostname2@address2[port2],...,hostnameN@addressN[portN]
* locators=hostname1[port1],hostname2[port2],...,hostnameN[portN]
* </code>
*
* Defaults to {@literal localhost[10334]}.
*/
@AliasFor("value")
String locators() default DEFAULT_LOCATORS;
/**
* Used to configure the Locators that a cluster will use in order to connect to a remote site in a multi-site
* (WAN) topology configuration.
*
* To use Locators in a WAN configuration, you must specify a unique distributed system ID ({@literal distributed-system-id})
* for the local cluster and remote Locator(s) for the remote clusters to which you will connect.
*
* For each remote Locator, provide a host name and/or address (separated by @, if you use both), followed by
* a port number in brackets.
*
* For example:
*
* <code>
* remote-locators=address1[port1],address2[port2],...,addressN[portN]
* remote-locators=hostname1@address1[port1],hostname2@address2[port2],...,hostnameN@addressN[portN]
* remote-locators=hostname1[port1],hostname2[port2],...,hostnameN[portN]
* </code>
*
* Defaults to unset.
*/
String remoteLocators() default DEFAULT_REMOTE_LOCATORS;
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2017-present 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.geode.config.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.client.ClientCache;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AliasFor;
/**
* The {@link UseMemberName} annotation configures the {@literal name} of the member in the Apache Geode
* distributed system, whether the member is a {@link ClientCache client} in the client/server topology
* or a {@link Cache peer Cache member} in the cluster using the P2P topology.
*
* @author John Blum
* @see java.lang.annotation.Documented
* @see java.lang.annotation.Inherited
* @see java.lang.annotation.Retention
* @see java.lang.annotation.Target
* @see org.apache.geode.cache.Cache
* @see org.apache.geode.cache.client.ClientCache
* @see org.springframework.context.annotation.Import
* @see org.springframework.core.annotation.AliasFor
* @see org.springframework.geode.config.annotation.MemberNameConfiguration
* @since 1.0.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Import(MemberNameConfiguration.class)
public @interface UseMemberName {
/**
* Alias for the {@link String name} of the Apache Geode distributed system member.
*
* @see #value()
*/
@AliasFor("value")
String name() default "";
/**
* {@link String Name} used for the Apache Geode distributed system member.
*
* @see #name()
*/
@AliasFor("name")
String value() default "";
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2017-present 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.geode.config.annotation.support;
import java.lang.annotation.Annotation;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport;
/**
* The {@link TypelessAnnotationConfigSupport} class is an extension of SDG's {@link AbstractAnnotationConfigSupport}
* based class for resolving {@link AnnotatedTypeMetadata}, however, is not based on any specific {@link Annotation}.
*
* @author John Blum
* @see java.lang.annotation.Annotation
* @see org.springframework.core.type.AnnotatedTypeMetadata
* @see org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport
* @since 1.2.0
*/
public class TypelessAnnotationConfigSupport extends AbstractAnnotationConfigSupport {
@Override
protected Class<? extends Annotation> getAnnotationType() {
return null;
}
}

View File

@@ -0,0 +1,342 @@
/*
* Copyright 2017-present 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.geode.context.annotation;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotatedBeanDefinitionReader;
import org.springframework.context.annotation.AnnotationConfigRegistry;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.context.annotation.ScopeMetadataResolver;
import org.springframework.context.support.AbstractRefreshableConfigApplicationContext;
import org.springframework.data.gemfire.config.annotation.PeerCacheApplication;
import org.springframework.data.gemfire.util.ArrayUtils;
import org.springframework.data.gemfire.util.SpringUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A {@literal refreshable} {@link ApplicationContext} capable of loading {@link Class component classes} used for
* {@link Annotation} based configuration in addition to scanning {@link String configuration locations}, and then
* providing the ability to reload/refresh the context at some point later during runtime.
*
* DISCLAIMER: Currently, this {@link ApplicationContext} implementation (and extension) is being used exclusively for
* testing and experimental ({@literal R&D}) purposes. It was designed around Apache Geode's forced-disconnect
* / auto-reconnect functionality, providing support for this behavior inside a Spring context. Specifically, this
* concern is only applicable when using Spring Boot to configure and bootstrap Apache Geode peer member
* {@link org.apache.geode.cache.Cache} applications, such as when annotating your Spring Boot application with
* SDG's {@link PeerCacheApplication} annotation. This {@link ApplicationContext} implementation is not recommended for
* use in Production Systems/Applications (yet).
*
* @author John Blum
* @see org.springframework.beans.factory.config.BeanDefinition
* @see org.springframework.beans.factory.support.BeanDefinitionRegistry
* @see org.springframework.beans.factory.support.BeanNameGenerator
* @see org.springframework.beans.factory.support.DefaultListableBeanFactory
* @see org.springframework.context.ApplicationContext
* @see org.springframework.context.annotation.AnnotatedBeanDefinitionReader
* @see org.springframework.context.annotation.AnnotationConfigRegistry
* @see org.springframework.context.annotation.ClassPathBeanDefinitionScanner
* @see org.springframework.context.annotation.ScopeMetadataResolver
* @see org.springframework.context.support.AbstractRefreshableConfigApplicationContext
* @since 1.3.0
*/
@SuppressWarnings("unused")
public class RefreshableAnnotationConfigApplicationContext extends AbstractRefreshableConfigApplicationContext
implements AnnotationConfigRegistry {
protected static final boolean DEFAULT_COPY_CONFIGURATION = false;
protected static final boolean USE_DEFAULT_FILTERS = true;
@Nullable
private BeanNameGenerator beanNameGenerator;
@Nullable
private volatile DefaultListableBeanFactory beanFactory;
private final Logger logger = LoggerFactory.getLogger(getClass());
private final Set<String> basePackages = new LinkedHashSet<>();
private final Set<Class<?>> componentClasses = new LinkedHashSet<>();
@Nullable
private ScopeMetadataResolver scopeMetadataResolver;
// TODO: WARNING - Calling refreshBeanFactory() in the constructor to eagerly create a BeanFactory is problematic.
// However, it does follow the BeanFactory creation pattern used by the GenericApplicationContext (and extensions
// like AnnotationConfigApplicationContext) that this RefreshableAnnotationConfigApplicationContext implementation
// is trying to preserve. Although, the AnnotationConfigApplicationContext implementation is NOT refreshable either,
// yet the AnnotationConfigWebApplicationContext is but doesn't eagerly create a BeanFactory during construction,
// so... The main problem with eagerly creating the BeanFactory in the constructor is the BeanFactory will be
// closed and reconstructed on refresh, as well as on each refresh thereafter.
/**
* Constructs an new instance of the {@link RefreshableAnnotationConfigApplicationContext}
* with default container state and no {@literal parent} {@link ApplicationContext}.
*
* @see #RefreshableAnnotationConfigApplicationContext(ApplicationContext)
*/
public RefreshableAnnotationConfigApplicationContext() {
this(null);
}
/**
* Constructs a new instance of the {@link RefreshableAnnotationConfigApplicationContext} initialized with
* the {@literal parent} {@link ApplicationContext}.
*
* Additionally, this constructor eagerly initializes a {@link ConfigurableListableBeanFactory},
* unlike {@link org.springframework.context.support.AbstractRefreshableApplicationContext} implementations,
* but exactly like {@link org.springframework.context.support.GenericApplicationContext} implementations.
*
* @param parent parent {@link ApplicationContext} to this child context.
* @see org.springframework.context.ApplicationContext
* @see #refreshBeanFactory()
*/
public RefreshableAnnotationConfigApplicationContext(@Nullable ApplicationContext parent) {
super(parent);
refreshBeanFactory();
}
/**
* Configures the {@link BeanNameGenerator} strategy used by this {@link ApplicationContext} to generate
* {@link String bean names} for {@link BeanDefinition bean definitions}.
*
* @param beanNameGenerator {@link BeanNameGenerator} used to generate {@link String bean names}
* for {@link BeanDefinition bean definitions}.
* @see org.springframework.beans.factory.support.BeanNameGenerator
*/
public void setBeanNameGenerator(@Nullable BeanNameGenerator beanNameGenerator) {
this.beanNameGenerator = beanNameGenerator;
}
/**
* Returns the {@link Optional optionally} configured {@link BeanNameGenerator} strategy used by this
* {@link ApplicationContext} to generate {@link String bean names} for {@link BeanDefinition bean definitions}.
*
* @return the {@link BeanNameGenerator} strategy used to generate {@link String bean names}
* for {@link BeanDefinition bean definitions}.
* @see org.springframework.beans.factory.support.BeanNameGenerator
* @see java.util.Optional
*/
protected Optional<BeanNameGenerator> getBeanNameGenerator() {
return Optional.ofNullable(this.beanNameGenerator);
}
/**
* Returns the configured {@link Logger} used to log framework messages to the application log.
*
* @return the configured {@link Logger}.
* @see org.slf4j.Logger
*/
protected @NonNull Logger getLogger() {
return this.logger;
}
/**
* Configures the {@link ScopeMetadataResolver} strategy used by this {@link ApplicationContext} to resolve
* the {@literal scope} for {@link BeanDefinition bean definitions}.
*
* @param scopeMetadataResolver {@link ScopeMetadataResolver} used to resolve the {@literal scope}
* of {@link BeanDefinition bean definitions}.
* @see org.springframework.context.annotation.ScopeMetadataResolver
*/
public void setScopeMetadataResolver(@Nullable ScopeMetadataResolver scopeMetadataResolver) {
this.scopeMetadataResolver = scopeMetadataResolver;
}
/**
* Returns the {@link Optional optionally} configured {@link ScopeMetadataResolver} strategy used by
* this {@link ApplicationContext} to resolve the {@literal scope} for {@link BeanDefinition bean definitions}.
*
* @return the configured {@link ScopeMetadataResolver} used to resolve the {@literal scope}
* for {@link BeanDefinition bean definitions}.
* @see org.springframework.context.annotation.ScopeMetadataResolver
* @see java.util.Optional
*/
public Optional<ScopeMetadataResolver> getScopeMetadataResolver() {
return Optional.ofNullable(this.scopeMetadataResolver);
}
protected boolean isCopyConfigurationEnabled() {
return DEFAULT_COPY_CONFIGURATION;
}
protected boolean isUsingDefaultFilters() {
return USE_DEFAULT_FILTERS;
}
/**
* Loads {@link BeanDefinition BeanDefinitions} from Annotation configuration (component) classes
* as well as from other resource locations (e.g. XML).
*
* @param beanFactory {@link DefaultListableBeanFactory} to configure.
* @throws BeansException if loading and configuring the {@link BeanDefinition BeanDefintions} for the target
* {@link DefaultListableBeanFactory} fails.
* @see org.springframework.beans.factory.support.DefaultListableBeanFactory
* @see #newAnnotatedBeanDefinitionReader(BeanDefinitionRegistry)
* @see #newClassBeanDefinitionScanner(BeanDefinitionRegistry)
* @see #getConfigLocations()
*/
@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException {
AnnotatedBeanDefinitionReader reader = configure(newAnnotatedBeanDefinitionReader(beanFactory));
ClassPathBeanDefinitionScanner scanner = configure(newClassBeanDefinitionScanner(beanFactory));
getBeanNameGenerator().ifPresent(beanNameGenerator -> {
reader.setBeanNameGenerator(beanNameGenerator);
scanner.setBeanNameGenerator(beanNameGenerator);
beanFactory.registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, beanNameGenerator);
});
getScopeMetadataResolver().ifPresent(scopeMetadataResolver -> {
reader.setScopeMetadataResolver(scopeMetadataResolver);
scanner.setScopeMetadataResolver(scopeMetadataResolver);
});
Arrays.stream(ArrayUtils.nullSafeArray(getConfigLocations(), String.class)).forEach(configLocation -> {
try {
Class<?> type = ClassUtils.forName(configLocation, getClassLoader());
getLogger().trace("Registering [{}]", configLocation);
reader.register(type);
}
catch (ClassNotFoundException cause) {
getLogger().trace(String.format("Could not load class for config location [%s] - trying package scan.",
configLocation), cause);
if (scanner.scan(configLocation) == 0) {
getLogger().debug("No component classes found for specified class/package [{}]", configLocation);
}
}
});
}
private AnnotatedBeanDefinitionReader configure(AnnotatedBeanDefinitionReader reader) {
Set<Class<?>> componentClasses = this.componentClasses;
if (!componentClasses.isEmpty()) {
getLogger().debug("Registering component classes: {}", componentClasses);
reader.register(ClassUtils.toClassArray(componentClasses));
}
return reader;
}
private ClassPathBeanDefinitionScanner configure(ClassPathBeanDefinitionScanner scanner) {
Set<String> basePackages = this.basePackages;
if (!basePackages.isEmpty()) {
getLogger().debug("Scanning base packages: {}", basePackages);
scanner.scan(StringUtils.toStringArray(basePackages));
}
return scanner;
}
protected AnnotatedBeanDefinitionReader newAnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry) {
return new AnnotatedBeanDefinitionReader(registry, getEnvironment());
}
protected ClassPathBeanDefinitionScanner newClassBeanDefinitionScanner(BeanDefinitionRegistry registry) {
return new ClassPathBeanDefinitionScanner(registry, isUsingDefaultFilters(), getEnvironment());
}
/**
* Re-registers Singleton beans registered with the previous {@link ConfigurableListableBeanFactory BeanFactory}
* (prior to refresh) with this {@link ApplicationContext}, iff this context was previously active
* and subsequently refreshed.
*
* @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory#copyConfigurationFrom(ConfigurableBeanFactory)
* @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerSingleton(String, Object)
* @see #getBeanFactory()
*/
@Override
protected void onRefresh() {
super.onRefresh();
ConfigurableListableBeanFactory currentBeanFactory = getBeanFactory();
if (this.beanFactory != null) {
Arrays.stream(ArrayUtils.nullSafeArray(this.beanFactory.getSingletonNames(), String.class))
.filter(singletonBeanName -> !currentBeanFactory.containsSingleton(singletonBeanName))
.forEach(singletonBeanName -> currentBeanFactory
.registerSingleton(singletonBeanName, this.beanFactory.getSingleton(singletonBeanName)));
if (isCopyConfigurationEnabled()) {
currentBeanFactory.copyConfigurationFrom(this.beanFactory);
}
}
}
/**
* Stores a reference to the previous {@link ConfigurableListableBeanFactory} in order to copy its configuration
* and state on {@link ApplicationContext} refresh invocations.
*
* @see #getBeanFactory()
*/
@Override
protected void prepareRefresh() {
this.beanFactory = (DefaultListableBeanFactory) SpringUtils.safeGetValue(this::getBeanFactory);
super.prepareRefresh();
}
/**
* @inheritDoc
*/
@Override
public void register(Class<?>... componentClasses) {
Arrays.stream(ArrayUtils.nullSafeArray(componentClasses, Class.class))
.filter(Objects::nonNull)
.forEach(this.componentClasses::add);
}
/**
* @inheritDoc
*/
@Override
public void scan(String... basePackages) {
Arrays.stream(ArrayUtils.nullSafeArray(basePackages, String.class))
.filter(StringUtils::hasText)
.forEach(this.basePackages::add);
}
}

View File

@@ -0,0 +1,237 @@
/*
* Copyright 2017-present 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.geode.context.logging;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.function.Function;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Spring {@link ApplicationListener} used to log the state of the Spring {@link Environment}
* on the {@link ContextRefreshedEvent}.
*
* @author John Blum
* @see org.slf4j.Logger
* @see org.slf4j.LoggerFactory
* @see org.springframework.context.ApplicationListener
* @see org.springframework.context.event.ContextRefreshedEvent
* @see org.springframework.core.env.Environment
* @see org.springframework.core.env.PropertySource
* @since 1.4.0
*/
@SuppressWarnings("unused")
public class EnvironmentLoggingApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
protected static final String SYSTEM_ERR_ENABLED_PROPERTY =
"spring.context.environment.logging.system-err.enabled";
static final ThreadLocal<Environment> threadLocalEnvironmentReference = new ThreadLocal<>();
private final Logger logger = LoggerFactory.getLogger(getClass());
/**
* @inheritDoc
*/
@Override
public void onApplicationEvent(@NonNull ContextRefreshedEvent contextRefreshedEvent) {
Environment environment = contextRefreshedEvent.getApplicationContext().getEnvironment();
threadLocalEnvironmentReference.set(environment);
try {
log("ENV: [%s]", ObjectUtils.nullSafeClassName(environment));
log("ENV: Active Profiles %s", Arrays.toString(environment.getActiveProfiles()));
log("ENV: Default Profiles %s", Arrays.toString(environment.getDefaultProfiles()));
if (environment instanceof ConfigurableEnvironment) {
ConfigurableEnvironment configurableEnvironment = (ConfigurableEnvironment) environment;
for (PropertySource<?> propertySource : configurableEnvironment.getPropertySources()) {
log("ENV: PropertySource [%s]", propertySource.getName());
getCompositePropertySourceLoggingFunction().apply(propertySource);
}
}
}
finally {
threadLocalEnvironmentReference.remove();
}
}
/**
* Returns a {@literal Composite} {@link Function} capable of introspecting and logging properties
* from specifically typed {@link PropertySource PropertySources}.
*
* @return a {@literal Composite} {@link Function} capable of introspecting and logging the properties
* from specifically typed {@link PropertySource PropertySources}.
* @see org.springframework.core.env.PropertySource
* @see java.util.function.Function
*/
protected Function<PropertySource<?>, PropertySource<?>> getCompositePropertySourceLoggingFunction() {
return new EnumerablePropertySourceLoggingFunction().andThen(new MapPropertySourceLoggingFunction());
}
/**
* Gets a reference to the configured SLF4J {@link Logger}.
*
* @return a reference to the configured SLF4J {@link Logger}.
* @see org.slf4j.Logger
*/
protected @NonNull Logger getLogger() {
return this.logger;
}
/**
* Logs the given {@link String message} formatted with the given array of {@link Object arguments}
* to the configured Spring Boot application log.
*
* The given {@link String message} will only be logged if it contains text, otherwise this method does nothing
* and silently returns.
*
* @param message {@link String} containing the message to log.
* @param args optional array of {@link Object arguments} to apply when formatting the {@link String message}.
* @see #logToSlf4jLogger(String, Object...)
* @see #logToSystemErr(String, Object...)
*/
protected void log(String message, Object... args) {
if (StringUtils.hasText(message)) {
logToSlf4jLogger(message, args);
logToSystemErr(message, args);
}
}
/**
* Logs the given {@link String message} to the configured SLF4J {@link Logger}.
*
* @param message {@link String} containing the message to log.
* @param args optional array of {@link Object arguments} to apply when formatting the {@link String message}.
* @see #getLogger()
*/
void logToSlf4jLogger(String message, Object... args) {
//getLogger().debug(String.format(message, args), args);
getLogger().debug(String.format(message, args));
}
/**
* Logs the given {@link String message} to {@link System#err}.
*
* This logging method is available to perform poor mans logging when explicit SLF4J {@link Logger} configuration
* was not provided in the deployed Spring Boot application. However, in most cases, this logging method should not
* be used and proper SLF4J {@link Logger} configuration should be provided in most cases.
*
* This logging option is only enabled when the {@literal spring.context.environment.logging.system-err.enabled}
* property is set to {@literal true}.
*
* @param message {@link String} containing the message to log.
* @param args optional array of {@link Object arguments} to apply when formatting the {@link String message}.
*/
void logToSystemErr(String message, Object... args) {
if (isSystemErrLoggingEnabled()) {
message = message.trim().endsWith("%n") ? message : message.concat("%n");
System.err.printf(message, args);
System.err.flush();
}
}
private boolean isSystemErrLoggingEnabled() {
return Optional.ofNullable(threadLocalEnvironmentReference.get())
.map(environment -> environment.getProperty(SYSTEM_ERR_ENABLED_PROPERTY, Boolean.class, false))
.orElseGet(() -> Boolean.getBoolean(SYSTEM_ERR_ENABLED_PROPERTY));
}
protected abstract class AbstractPropertySourceLoggingFunction
implements Function<PropertySource<?>, PropertySource<?>> {
protected void logProperties(@NonNull Iterable<String> propertyNames,
@NonNull Function<String, Object> propertyValueFunction) {
log("Properties [");
for (String propertyName : CollectionUtils.nullSafeIterable(propertyNames)) {
log("\t%1$s = %2$s", propertyName, propertyValueFunction.apply(propertyName));
}
log("]");
}
}
protected class EnumerablePropertySourceLoggingFunction extends AbstractPropertySourceLoggingFunction {
@Override
public @Nullable PropertySource<?> apply(@Nullable PropertySource<?> propertySource) {
if (propertySource instanceof EnumerablePropertySource) {
EnumerablePropertySource<?> enumerablePropertySource =
(EnumerablePropertySource<?>) propertySource;
String[] propertyNames = enumerablePropertySource.getPropertyNames();
Arrays.sort(propertyNames);
logProperties(Arrays.asList(propertyNames), enumerablePropertySource::getProperty);
}
return propertySource;
}
}
// The PropertySource may not be enumerable but may use a Map as its source.
protected class MapPropertySourceLoggingFunction extends AbstractPropertySourceLoggingFunction {
@Override
@SuppressWarnings("unchecked")
public @Nullable PropertySource<?> apply(@Nullable PropertySource<?> propertySource) {
if (!(propertySource instanceof EnumerablePropertySource)) {
Object source = propertySource != null
? propertySource.getSource()
: null;
if (source instanceof Map) {
Map<String, Object> map = new TreeMap<>((Map<String, Object>) source);
logProperties(map.keySet(), map::get);
}
}
return propertySource;
}
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright 2017-present 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.geode.context.logging;
import java.util.Arrays;
import java.util.Properties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.context.logging.LoggingApplicationListener;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.GenericApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.core.env.Environment;
import org.springframework.data.gemfire.config.annotation.EnableLogging;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Spring {@link GenericApplicationListener} used to configure Apache Geode Logging from existing Spring Data
* for Apache Geode Logging configuration support, such as when using the {@link EnableLogging} annotation
* or alternatively using {@link Properties}.
*
* This listener must be ordered before the Spring Boot {@link LoggingApplicationListener}.
*
* @author John Blum
* @see java.util.Properties
* @see org.springframework.boot.SpringApplication
* @see org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
* @see org.springframework.boot.context.logging.LoggingApplicationListener
* @see org.springframework.context.ApplicationContext
* @see org.springframework.context.ApplicationEvent
* @see org.springframework.context.event.GenericApplicationListener
* @see org.springframework.core.Ordered
* @see org.springframework.core.env.Environment
* @since 1.3.0
*/
public class GeodeLoggingApplicationListener implements GenericApplicationListener {
private static final Class<?>[] EVENT_TYPES = { ApplicationEnvironmentPreparedEvent.class };
private static final Class<?>[] SOURCE_TYPES = { ApplicationContext.class, SpringApplication.class };
public static final String SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY = "spring.boot.data.gemfire.log.level";
public static final String SPRING_DATA_GEMFIRE_CACHE_LOG_LEVEL = "spring.data.gemfire.cache.log-level";
public static final String SPRING_DATA_GEMFIRE_LOGGING_LOG_LEVEL = "spring.data.gemfire.logging.level";
@Override
public int getOrder() {
return LoggingApplicationListener.DEFAULT_ORDER > Ordered.HIGHEST_PRECEDENCE
? LoggingApplicationListener.DEFAULT_ORDER - 1
: Ordered.HIGHEST_PRECEDENCE;
}
@Override
public void onApplicationEvent(@Nullable ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
ApplicationEnvironmentPreparedEvent environmentPreparedEvent = (ApplicationEnvironmentPreparedEvent) event;
onApplicationEnvironmentPreparedEvent(environmentPreparedEvent);
}
}
protected void onApplicationEnvironmentPreparedEvent(
@NonNull ApplicationEnvironmentPreparedEvent environmentPreparedEvent) {
Assert.notNull(environmentPreparedEvent, "ApplicationEnvironmentPreparedEvent must not be null");
Environment environment = environmentPreparedEvent.getEnvironment();
if (isSystemPropertyNotSet(SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY)) {
String logLevel = environment.getProperty(SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY,
environment.getProperty(SPRING_DATA_GEMFIRE_LOGGING_LOG_LEVEL,
environment.getProperty(SPRING_DATA_GEMFIRE_CACHE_LOG_LEVEL)));
setSystemProperty(SPRING_BOOT_DATA_GEMFIRE_LOG_LEVEL_PROPERTY, logLevel);
}
}
protected boolean isSystemPropertySet(@Nullable String propertyName) {
return StringUtils.hasText(propertyName) && StringUtils.hasText(System.getProperty(propertyName));
}
protected boolean isSystemPropertyNotSet(@Nullable String propertyName) {
return !isSystemPropertySet(propertyName);
}
protected void setSystemProperty(@NonNull String propertyName, @Nullable String propertyValue) {
Assert.hasText(propertyName, () -> String.format("PropertyName [%s] is required", propertyName));
if (StringUtils.hasText(propertyValue)) {
System.setProperty(propertyName, propertyValue);
}
}
@Override
public boolean supportsEventType(@NonNull ResolvableType eventType) {
Class<?> rawType = eventType.getRawClass();
return rawType != null && Arrays.stream(EVENT_TYPES).anyMatch(it -> it.isAssignableFrom(rawType));
}
@Override
public boolean supportsSourceType(@Nullable Class<?> sourceType) {
return sourceType != null && Arrays.stream(SOURCE_TYPES).anyMatch(it -> it.isAssignableFrom(sourceType));
}
}

View File

@@ -0,0 +1,220 @@
/*
* Copyright 2017-present 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.geode.core.env;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newUnsupportedOperationException;
import java.util.AbstractMap;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* {@link Map} implementation adapting an {@link Environment} object in order to use the {@link Environment}
* as a {@link Map}.
*
* @author John Blum
* @see java.util.Map
* @see java.util.AbstractMap
* @see org.springframework.core.env.Environment
* @see org.springframework.core.env.PropertySource
* @see <a href="https://en.wikipedia.org/wiki/Adapter_pattern">Adapter Software Design Pattern</a>
* @since 1.3.1
*/
public class EnvironmentMapAdapter extends AbstractMap<String, String> {
/**
* Factory method used to construct an new instance of {@link EnvironmentMapAdapter} initialized with
* the given {@link Environment}.
*
* @param environment {@link Environment} to adapt; must not be {@literal null}.
* @return a new instance of {@link EnvironmentMapAdapter} for the given {@link Environment}.
* @throws IllegalArgumentException if {@link Environment} is {@literal null}.
* @see org.springframework.core.env.Environment
* @see #EnvironmentMapAdapter(Environment)
*/
public static EnvironmentMapAdapter from(@NonNull Environment environment) {
return new EnvironmentMapAdapter(environment);
}
private final Environment environment;
/**
* Constructs a new instance of {@link EnvironmentMapAdapter} initialized with the given {@link Environment}.
*
* @param environment {@link Environment} to adapt; must not be {@literal null}.
* @throws IllegalArgumentException if {@link Environment} is {@literal null}.
* @see org.springframework.core.env.Environment
*/
public EnvironmentMapAdapter(@NonNull Environment environment) {
Assert.notNull(environment, "Environment must not be null");
this.environment = environment;
}
/**
* Gets the configured {@link Environment} object being adapted by this {@link Map}.
*
* @return the configured {@link Environment}; never {@literal null}.
* @see org.springframework.core.env.Environment
*/
protected @NonNull Environment getEnvironment() {
return this.environment;
}
/**
* Null-safe method determining whether the given {@link Object key} is a property
* in the underlying {@link Environment}.
*
* @return a boolean value indicating whether the given {@link Object key} is a property
* in the underlying {@link Environment}.
* @see org.springframework.core.env.Environment#containsProperty(String)
* @see #getEnvironment()
*/
@Override
public boolean containsKey(@Nullable Object key) {
return key != null && getEnvironment().containsProperty(String.valueOf(key));
}
/**
* Gets the {@link String value} for the property identified by the given {@link Map} {@link Object key}
* from the underlying {@link Environment}.
*
* @param key {@link Object key} identifying the property whose value will be retrieved from the {@link Environment}.
* @return the {@link String value} of the property identified by the given {@link Map} {@link Object key}
* from the {@link Environment}.
* @see org.springframework.core.env.Environment#getProperty(String)
* @see #getEnvironment()
*/
@Override
public @Nullable String get(@Nullable Object key) {
return key != null ? getEnvironment().getProperty(String.valueOf(key)) : null;
}
/**
* @inheritDoc
*/
@Override
public Set<Entry<String, String>> entrySet() {
Environment environment = getEnvironment();
if (environment instanceof ConfigurableEnvironment) {
Set<Entry<String, String>> entrySet = new HashSet<>();
for (PropertySource<?> propertySource : ((ConfigurableEnvironment) environment).getPropertySources()) {
if (propertySource instanceof EnumerablePropertySource) {
for (String propertyName : ((EnumerablePropertySource<?>) propertySource).getPropertyNames()) {
entrySet.add(new EnvironmentEntry(environment, propertyName));
}
}
}
return Collections.unmodifiableSet(entrySet);
}
throw newUnsupportedOperationException("Unable to determine the entrySet from the Environment [%s]",
getEnvironment().getClass().getName());
}
/**
* {@link EnvironmentEntry} is a {@code Map.Entry} implementation mapping an {@link Environment} property (key)
* to its value.
*
* @see java.util.Map.Entry
* @see org.springframework.core.env.Environment
*/
protected static class EnvironmentEntry implements Map.Entry<String, String> {
private final Environment environment;
private final String key;
/**
* Constructs a new instance of {@link EnvironmentEntry} initialized with the given {@link Environment}
* and {@link String key} (property).
*
* @param environment {@link Environment} to which the {@link String key} belongs; must not be {@literal null}.
* @param key {@link String} referring to the property from the {@link Environment}; must not be {@literal null}.
* @throws IllegalArgumentException if the {@link Environment} or the {@link String key} is {@literal null}.
* @see org.springframework.core.env.Environment
*/
public EnvironmentEntry(@NonNull Environment environment, @NonNull String key) {
Assert.notNull(environment, "Environment must not be null");
Assert.hasText(key, () -> String.format("Key [%s] must be specified", key));
this.environment = environment;
this.key = key;
}
/**
* Returns the configured {@link Environment} to which this {@code Map.Entry} belongs.
*
* @return the configured {@link Environment}; never {@literal null}.
* @see org.springframework.core.env.Environment
*/
protected @NonNull Environment getEnvironment() {
return this.environment;
}
/**
* Gets the {@link String key} (property) of this {@code Map.Entry}.
*
* @return the {@link String key} (property) of this {@code Map.Entry}.
*/
@Override
public @NonNull String getKey() {
return this.key;
}
/**
* Gets the {@link String value} mapped to the {@link #getKey() key} (property) in this {@code Map.Entry}
* ({@link Environment}).
*
* @return the {@link String value} mapped to the {@link #getKey() key} (property) in this {@code Map.Entry}
* ({@link Environment}).
* @see org.springframework.core.env.Environment#getProperty(String)
* @see #getEnvironment()
* @see #getKey()
*/
@Override
public @Nullable String getValue() {
return getEnvironment().getProperty(getKey());
}
/**
* @inheritDoc
* @throws UnsupportedOperationException
*/
@Override
public String setValue(String value) {
throw newUnsupportedOperationException("Setting the value of Environment property [%s] is not supported",
getKey());
}
}
}

View File

@@ -0,0 +1,379 @@
/*
* Copyright 2017-present 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.geode.core.env;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalArgumentException;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.geode.core.env.support.CloudCacheService;
import org.springframework.geode.core.env.support.Service;
import org.springframework.geode.core.env.support.User;
import org.springframework.geode.core.util.ObjectUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* The {@link VcapPropertySource} class is a Spring {@link PropertySource} to process
* {@literal VCAP} environment properties in Pivotal CloudFoundry.
*
* @author John Blum
* @see java.lang.Iterable
* @see java.net.URL
* @see java.util.Properties
* @see java.util.function.Predicate
* @see org.springframework.core.env.ConfigurableEnvironment
* @see org.springframework.core.env.EnumerablePropertySource
* @see org.springframework.core.env.Environment
* @see org.springframework.core.env.PropertiesPropertySource
* @see org.springframework.core.env.PropertySource
* @see org.springframework.geode.core.env.support.CloudCacheService
* @see org.springframework.geode.core.env.support.Service
* @see org.springframework.geode.core.env.support.User
* @since 1.0.0
*/
@SuppressWarnings("unused")
public class VcapPropertySource extends PropertySource<EnumerablePropertySource<?>> implements Iterable<String> {
private static final String CLOUD_CACHE_TAG_NAME = "cloudcache";
private static final String GEMFIRE_TAG_NAME = "gemfire";
private static final String THIS_PROPERTY_SOURCE_NAME = "boot.data.gemfire.vcap";
private static final String VCAP_APPLICATION_PROPERTY = "vcap.application.";
private static final String VCAP_APPLICATION_NAME_PROPERTY = VCAP_APPLICATION_PROPERTY + "name";
private static final String VCAP_APPLICATION_URIS_PROPERTY = VCAP_APPLICATION_PROPERTY + "uris";
private static final String VCAP_PROPERTY_SOURCE_NAME = "vcap";
private static final String VCAP_SERVICES_PROPERTY = "vcap.services.";
private static final String VCAP_SERVICES_SERVICE_NAME_LOCATORS_PROPERTY = VCAP_SERVICES_PROPERTY + "%s.credentials.locators";
private static final String VCAP_SERVICES_SERVICE_NAME_NAME_PROPERTY = VCAP_SERVICES_PROPERTY + "%s.name";
private static final String VCAP_SERVICES_SERVICE_NAME_TLS_ENABLED_PROPERTY = VCAP_SERVICES_PROPERTY + "%s.credentials.tls-enabled";
private static final String VCAP_SERVICES_SERVICE_NAME_URL_GFSH_PROPERTY = VCAP_SERVICES_PROPERTY + "%s.credentials.urls.gfsh";
private static final String VCAP_SERVICES_SERVICE_NAME_USERS_PROPERTY = VCAP_SERVICES_PROPERTY + "%s.credentials.users";
private static final String VCAP_SERVICES_SERVICE_NAME_USERS_INDEX_PROPERTY = VCAP_SERVICES_SERVICE_NAME_USERS_PROPERTY + "[%d]";
private static final Predicate<Object> CLOUD_CACHE_SERVICE_PREDICATE =
propertyValue -> String.valueOf(propertyValue).toLowerCase().contains(CLOUD_CACHE_TAG_NAME);
private static final Predicate<Object> GEMFIRE_SERVICE_PREDICATE =
propertyValue -> String.valueOf(propertyValue).toLowerCase().contains(GEMFIRE_TAG_NAME);
private static final Predicate<Object> CLOUD_CACHE_AND_GEMFIRE_SERVICE_PREDICATE =
CLOUD_CACHE_SERVICE_PREDICATE.and(GEMFIRE_SERVICE_PREDICATE);
private static final Predicate<String> VCAP_APPLICATION_PROPERTIES_PREDICATE =
propertyName -> String.valueOf(propertyName).trim().toLowerCase().startsWith(VCAP_APPLICATION_PROPERTY);
private static final Predicate<PropertySource<?>> VCAP_REQUIRED_PROPERTIES_PREDICATE =
propertySource -> propertySource.containsProperty(VCAP_APPLICATION_NAME_PROPERTY)
&& propertySource.containsProperty(VCAP_APPLICATION_URIS_PROPERTY);
private static final Predicate<String> VCAP_SERVICES_PROPERTIES_PREDICATE =
propertyName -> String.valueOf(propertyName).trim().toLowerCase().startsWith(VCAP_SERVICES_PROPERTY);
public static VcapPropertySource from(Environment environment) {
return Optional.ofNullable(environment)
.filter(ConfigurableEnvironment.class::isInstance)
.map(ConfigurableEnvironment.class::cast)
.map(ConfigurableEnvironment::getPropertySources)
.map(propertySources -> propertySources.get(VCAP_PROPERTY_SOURCE_NAME))
.map(VcapPropertySource::from)
.orElseThrow(() -> newIllegalArgumentException(
"Environment was not configurable or does not contain an enumerable [%s] PropertySource",
VCAP_PROPERTY_SOURCE_NAME));
}
public static VcapPropertySource from(Properties properties) {
return Optional.ofNullable(properties)
.map(it -> new PropertiesPropertySource(THIS_PROPERTY_SOURCE_NAME, properties))
.filter(VCAP_REQUIRED_PROPERTIES_PREDICATE)
.map(VcapPropertySource::new)
.orElseThrow(() -> newIllegalArgumentException("Properties are required"));
}
public static VcapPropertySource from(PropertySource<?> propertySource) {
return Optional.ofNullable(propertySource)
.filter(it -> VCAP_PROPERTY_SOURCE_NAME.equals(it.getName()))
.filter(VCAP_REQUIRED_PROPERTIES_PREDICATE)
.filter(EnumerablePropertySource.class::isInstance)
.map(EnumerablePropertySource.class::cast)
.map(VcapPropertySource::new)
.orElseThrow(() -> newIllegalArgumentException(
"An EnumerablePropertySource named [%s] containing VCAP properties is required",
VCAP_PROPERTY_SOURCE_NAME));
}
private Predicate<String> vcapServicePredicate;
/**
* Constructs a new {@link PropertySource} from the existing, required {@link EnumerablePropertySource} instance
* with the default name, {@literal boot.data.gemfire.vcap}, containing the {@literal VCAP} environment variable
* configuration.
*
* @param propertySource existing, required {@link EnumerablePropertySource} containing the {@literal VCAP}
* environment variables.
* @throws IllegalArgumentException if the {@literal EnumerablePropertySource} is {@literal null}.
* @see org.springframework.core.env.EnumerablePropertySource
*/
private VcapPropertySource(EnumerablePropertySource<?> propertySource) {
super(THIS_PROPERTY_SOURCE_NAME, propertySource);
}
protected Set<String> findAllPropertiesByNameMatching(Predicate<String> predicate) {
return findAllPropertiesByNameMatching(this, predicate);
}
protected Set<String> findAllPropertiesByNameMatching(Iterable<String> properties, Predicate<String> predicate) {
return StreamSupport.stream(CollectionUtils.nullSafeIterable(properties).spliterator(), false)
.filter(predicate)
.collect(Collectors.toSet());
}
protected Set<String> findAllPropertiesByValueMatching(Predicate<Object> predicate) {
return findAllPropertiesByValueMatching(this, predicate);
}
protected Set<String> findAllPropertiesByValueMatching(Iterable<String> properties, Predicate<Object> predicate) {
return StreamSupport.stream(CollectionUtils.nullSafeIterable(properties).spliterator(), false)
.filter(propertyName -> predicate.test(getProperty(propertyName)))
.collect(Collectors.toSet());
}
public Set<String> findAllVcapApplicationProperties() {
return findAllPropertiesByNameMatching(VCAP_APPLICATION_PROPERTIES_PREDICATE);
}
public Set<String> findAllVcapServicesProperties() {
return findTargetVcapServiceProperties(VCAP_SERVICES_PROPERTIES_PREDICATE);
}
public Set<String> findTargetVcapServiceProperties(Predicate<String> vcapServicePropertiesPredicate) {
return findAllPropertiesByNameMatching(filterByVcapServicePropertiesPredicate(vcapServicePropertiesPredicate));
}
private Predicate<String> filterByVcapServicePropertiesPredicate(Predicate<String> vcapServicePropertiesPredicate) {
return isValid(vcapServicePropertiesPredicate)
? VCAP_SERVICES_PROPERTIES_PREDICATE.and(vcapServicePropertiesPredicate)
: VCAP_SERVICES_PROPERTIES_PREDICATE;
}
private boolean isValid(Predicate<String> vcapServicePropertiesPredicate) {
return vcapServicePropertiesPredicate != null
&& vcapServicePropertiesPredicate != VCAP_SERVICES_PROPERTIES_PREDICATE;
}
public Optional<CloudCacheService> findFirstCloudCacheService() {
return findFirstCloudCacheServiceName()
.map(serviceName -> {
CloudCacheService service = CloudCacheService.with(serviceName);
Object locators = getProperty(String.format(VCAP_SERVICES_SERVICE_NAME_LOCATORS_PROPERTY, service));
Optional.ofNullable(locators)
.map(String::valueOf)
.filter(StringUtils::hasText)
.ifPresent(service::withLocators);
Object tlsEnabled = getProperty(String.format(VCAP_SERVICES_SERVICE_NAME_TLS_ENABLED_PROPERTY, service));
Optional.ofNullable(tlsEnabled)
.map(String::valueOf)
.map(Boolean::parseBoolean)
.ifPresent(service::withTls);
Object gfshUrl = getProperty(String.format(VCAP_SERVICES_SERVICE_NAME_URL_GFSH_PROPERTY, service));
Optional.ofNullable(gfshUrl)
.map(String::valueOf)
.filter(StringUtils::hasText)
.map(urlString -> ObjectUtils.doOperationSafely(() -> new URL(urlString)))
.ifPresent(service::withGfshUrl);
return service;
});
}
public CloudCacheService requireFirstCloudCacheService() {
return findFirstCloudCacheService().orElseThrow(() ->
newIllegalStateException("Unable to resolve a CloudCache Service Instance"));
}
public Optional<String> findFirstCloudCacheServiceName() {
Iterable<String> vcapServicesProperties = findTargetVcapServiceProperties(getVcapServicePredicate());
return findAllPropertiesByValueMatching(vcapServicesProperties, CLOUD_CACHE_AND_GEMFIRE_SERVICE_PREDICATE)
.stream()
.filter(propertyName -> propertyName.endsWith(".tags"))
.map(propertyName -> propertyName.substring(VCAP_SERVICES_PROPERTY.length()))
.map(propertyName -> propertyName.substring(0, propertyName.indexOf(".")))
.filter(StringUtils::hasText)
.min(String.CASE_INSENSITIVE_ORDER);
}
public String requireFirstCloudCacheServiceName() {
String tags = String.format("%1$s, %2$s", CLOUD_CACHE_TAG_NAME, GEMFIRE_TAG_NAME);
return findFirstCloudCacheServiceName()
.orElseThrow(() -> newIllegalStateException("No service with tags [%s] was found", tags));
}
public Optional<User> findUserByName(Service service, String targetUsername) {
Assert.hasText(targetUsername, String.format("Target username [%s] is required", targetUsername));
Optional<User> optionalUser = Optional.empty();
String serviceName = service.getName();
String userPropertyName = String.format(VCAP_SERVICES_SERVICE_NAME_USERS_INDEX_PROPERTY, serviceName, 0);
for (int index = 1; containsProperty(asUserUsernameProperty(userPropertyName)); index++) {
String username = String.valueOf(getProperty(asUserUsernameProperty(userPropertyName)));
if (username.equals(targetUsername)) {
break;
}
userPropertyName = String.format(VCAP_SERVICES_SERVICE_NAME_USERS_INDEX_PROPERTY, serviceName, index);
}
if (containsProperty(asUserUsernameProperty(userPropertyName))) {
String username = String.valueOf(getProperty(asUserUsernameProperty(userPropertyName)));
String password = String.valueOf(getProperty(asUserPasswordProperty(userPropertyName)));
User user = User.with(username)
.withPassword(password);
optionalUser = Optional.of(user);
}
return optionalUser;
}
public Optional<User> findFirstUserByRoleClusterOperator(Service service) {
Optional<User> optionalUser = Optional.empty();
String serviceName = service.getName();
String userPropertyName = String.format(VCAP_SERVICES_SERVICE_NAME_USERS_INDEX_PROPERTY, serviceName, 0);
for (int index = 1; containsProperty(asUserRolesProperty(userPropertyName)); index++) {
String roles = String.valueOf(getProperty(asUserRolesProperty(userPropertyName)));
if (roles.contains(User.Role.CLUSTER_OPERATOR.name().toLowerCase())) {
break;
}
userPropertyName = String.format(VCAP_SERVICES_SERVICE_NAME_USERS_INDEX_PROPERTY, serviceName, index);
}
if (containsProperty(asUserUsernameProperty(userPropertyName))) {
String username = String.valueOf(getProperty(asUserUsernameProperty(userPropertyName)));
String password = String.valueOf(getProperty(asUserPasswordProperty(userPropertyName)));
User user = User.with(username)
.withPassword(password)
.withRole(User.Role.CLUSTER_OPERATOR);
optionalUser = Optional.of(user);
}
return optionalUser;
}
private String asUserPasswordProperty(String userProperty) {
return String.format("%s.password", userProperty);
}
private String asUserRolesProperty(String userProperty) {
return String.format("%s.roles", userProperty);
}
private String asUserUsernameProperty(String userProperty) {
return String.format("%s.username", userProperty);
}
@Nullable
@Override
public Object getProperty(String name) {
return getSource().getProperty(name);
}
@NonNull
protected Predicate<String> getVcapServicePredicate() {
return this.vcapServicePredicate != null
? this.vcapServicePredicate
: propertyName -> true;
}
@Override
public Iterator<String> iterator() {
return Collections.unmodifiableList(Arrays.asList(getSource().getPropertyNames())).iterator();
}
@NonNull
public VcapPropertySource withVcapServiceName(@NonNull String serviceName) {
Assert.hasText(serviceName, "Service name is required");
String resolvedServiceName = StringUtils.trimAllWhitespace(serviceName);
Predicate<String> vcapServiceNamePredicate = propertyName ->
propertyName.startsWith(String.format("%1$s%2$s.", VCAP_SERVICES_PROPERTY, resolvedServiceName));
return withVcapServicePredicate(vcapServiceNamePredicate);
}
@NonNull
public VcapPropertySource withVcapServicePredicate(@Nullable Predicate<String> vcapServicePredicate) {
this.vcapServicePredicate = vcapServicePredicate;
return this;
}
}

View File

@@ -0,0 +1,378 @@
/*
* Copyright 2017-present 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.geode.core.env.support;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalArgumentException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.data.gemfire.GemfireUtils;
import org.springframework.geode.core.util.ObjectUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* The {@link CloudCacheService} class is an Abstract Data Type (ADT) modeling the Pivotal Cloud Cache service
* in Pivotal CloudFoundry (PCF).
*
* @author John Blum
* @see java.net.URL
* @see org.springframework.geode.core.env.support.Service
* @since 1.0.0
*/
public class CloudCacheService extends Service {
/**
* Factory method used to construct a new {@link CloudCacheService} initialized with the given {@link String name}.
*
* @param name {@link String} containing the name of the {@link CloudCacheService}.
* @throws IllegalArgumentException if the {@link String name} is {@literal null} or empty.
* @return the new {@link CloudCacheService} with the given {@link String name}.
* @see #CloudCacheService(String)
*/
public static CloudCacheService with(String name) {
return new CloudCacheService(name);
}
private Boolean tlsEnabled;
private String locators;
private URL gfshUrl;
/**
* Construct a new instance of {@link CloudCacheService} initialized with the given {@link String name}.
*
* @param name {@link String} containing the name of the {@link CloudCacheService}.
* @throws IllegalArgumentException if the {@link String name} is {@literal null} or empty.
*/
private CloudCacheService(String name) {
super(name);
}
/**
* Returns an {@link Optional} Gfsh {@link URL} if configured, used to connect to Apache Geode's Management REST API
* (service).
*
* @return an {@link Optional} Gfsh {@link URL} used to connect to Apache Geode's Management REST API (service).
* @see #withGfshUrl(URL)
* @see java.util.Optional
* @see java.net.URL
*/
public Optional<URL> getGfshUrl() {
return Optional.ofNullable(this.gfshUrl);
}
/**
* Returns an {@link Optional} {@link String} containing the list of Apache Geode Locator network endpoints.
*
* The format of the {@link String}, if present, is {@literal host1[port1],host2[port2], ...,hostN[portN]}.
*
* @return an {@link Optional} {@link String} containing the list of Apache Geode Locator network endpoints.
* @see #withLocators(String)
*/
public Optional<String> getLocators() {
return Optional.ofNullable(this.locators)
.filter(StringUtils::hasText);
}
/**
* Returns a {@link List} of Apache Geode Locator network endpoints.
*
* Returns an {@link Collections#emptyList() empty List} if no Locators were configured.
*
* @return a {@link List} of Apache Geode Locator network endpoints.
* @see #getLocators()
*/
public List<Locator> getLocatorList() {
return getLocators()
.map(Locator::parseLocators)
.orElseGet(Collections::emptyList);
}
/**
* Returns a {@link Boolean} determining whether TLS/SSL is enabled between the client and the Pivotal Cloud Cache
* (PCC) service instance in Pivotal CloudFoundry (PCF).
*
* @return {@literal true} if TLS is enabled, {@literal false} if not.
*/
public boolean isTlsEnabled() {
return Boolean.TRUE.equals(this.tlsEnabled);
}
/**
* Builder method used to configure the Gfsh {@link URL} to connect to the Apache Geode
* Management REST API (service).
*
* @param gfshUrl {@link URL} used to connect to the Apache Geode Management REST API (service).
* @return this {@link CloudCacheService}.
* @see #getGfshUrl()
*/
public CloudCacheService withGfshUrl(URL gfshUrl) {
this.gfshUrl = gfshUrl;
return this;
}
/**
* Builder method used to configure the {@link String list of Locator} network endpoints.
*
* @param locators {@link String} containing a comma-delimited list of Locator network endpoints
* of the format: {@literal host1[port1],host2[port2], ...,hostN[portN]}.
* @return this {@link CloudCacheService}.
* @see #getLocators()
*/
public CloudCacheService withLocators(String locators) {
this.locators = locators;
return this;
}
/**
* Builder method used to configure whether TLS/SSL is enabled between a client and the Pivotal Cloud Cache (PCC)
* service instance in Pivotal CloudFoundry (PCF).
*
* @param enabled {@link Boolean} value indicating whether TLS/SSL is enabled.
* @return this {@link CloudCacheService}.
* @see #isTlsEnabled()
*/
public CloudCacheService withTls(Boolean enabled) {
this.tlsEnabled = enabled;
return this;
}
public static class Locator implements Comparable<Locator> {
static final int DEFAULT_LOCATOR_PORT = GemfireUtils.DEFAULT_LOCATOR_PORT;
static final String DEFAULT_LOCATOR_HOST = "localhost";
private Integer port;
private String host;
/**
* Factory method used to construct a new {@link Locator} on the default {@link String host}
* and {@link Integer port}.
*
* @return a new, default {@link Locator}.
* @see #newLocator(String, int)
*/
public static Locator newLocator() {
return newLocator(DEFAULT_LOCATOR_HOST, DEFAULT_LOCATOR_PORT);
}
/**
* Factory method used to construct a new {@link Locator} running on the default {@link String host}
* and configured to listen on the given {@link Integer port}.
*
* @param port {@link Integer} containing the port number on which the {@link Locator} is listening.
* @return a new {@link Locator} running on the default {@link String host},
* listening on the given {@link Integer port}.
* @throws IllegalArgumentException if the {@link Integer port} is less than {@literal 0}.
* @see #newLocator(String, int)
*/
public static Locator newLocator(int port) {
return newLocator(DEFAULT_LOCATOR_HOST, port);
}
/**
* Factory method used to construct a new {@link Locator} configured to run on the given {@link String host}
* and listening on the default {@link Integer port}.
*
* @param host {@link String} containing the name of the host on which the {@link Locator} is running.
* @return a new {@link Locator} running on the configured {@link String host},
* listening on the default {@link Integer port}.
* @throws IllegalArgumentException if {@link String host} is {@literal null} or empty.
* @see #newLocator(String, int)
*/
public static Locator newLocator(String host) {
return newLocator(host, DEFAULT_LOCATOR_PORT);
}
/**
* Factory method used to construct a new {@link Locator} running on the configured {@link String host}
* and listening on the configured {@link Integer port}.
*
* @param host {@link String} containing the name of the host on which the {@link Locator} is running.
* @param port {@link Integer} containing the port number on which the {@link Locator} is listening.
* @throws IllegalArgumentException if {@link String host} is {@literal null} or empty,
* or the {@link Integer port} is less than {@literal 0}.
* @return a new {@link Locator} on the configured {@link String host} and {@link Integer port}.
*/
public static Locator newLocator(String host, int port) {
Assert.hasText(host, String.format("Host [%s] is required", host));
Assert.isTrue(port > -1, String.format("Port [%d] must be greater than equal to 0", port));
return new Locator(host, port);
}
/**
* Factory method used to parse a {@link String comma-delimited list of Locator network endpoints}
* into a {@link List} of {@link Locator} objects.
*
* The {@link String comma-delimited list of Locators} must be formatted as
* {@literal host1[port1],host2[port2], ...,hostN[portN]}.
*
* @param locators {@link String} containing a comma-delimited list of Locator network endpoints.
* @return a new {@link List} of {@link Locator} objects or an empty {@link List}
* if no Locators were specified.
* @throws IllegalArgumentException if an individual Locator {@link String host[port]} is not valid.
* @see #parse(String)
*/
public static List<Locator> parseLocators(String locators) {
return Arrays.stream(String.valueOf(locators).split(","))
.filter(StringUtils::hasText)
.map(Locator::parse)
.collect(Collectors.toList());
}
/**
* Factory method used to parse an individual {@link String host[port]} network endpoint for a Locator.
*
* @param hostPort {@link String} containing the Locator host and port to parse.
* @return a new {@link Locator} configured from the given {@link String}.
* @throws IllegalArgumentException if the {@link String hostPort} are not valid.
* @see #parseHost(String)
* @see #parsePort(String)
* @see #newLocator(String, int)
*/
public static Locator parse(String hostPort) {
return Optional.ofNullable(hostPort)
.filter(StringUtils::hasText)
.map(it -> {
String host = parseHost(it);
int port = parsePort(it);
return newLocator(host, port);
})
.orElseThrow(() -> newIllegalArgumentException("Locator host/port [%s] is not valid", hostPort));
}
private static String parseHost(String value) {
int index = String.valueOf(value).trim().indexOf("[");
return index > 0 ? value.trim().substring(0, index).trim()
: index != 0 && StringUtils.hasText(value) ? value.trim()
: DEFAULT_LOCATOR_HOST;
}
private static int parsePort(String value) {
StringBuilder digits = new StringBuilder();
for (char chr : String.valueOf(value).toCharArray()) {
if (Character.isDigit(chr)) {
digits.append(chr);
}
}
return digits.length() > 0 ? Integer.parseInt(digits.toString()) : DEFAULT_LOCATOR_PORT;
}
/**
* Construct a new {@link Locator} initialized with the {@link String host} and {@link Integer port}
* on which this {@link Locator} is running and listening for connections.
*
* @param host {@link String} containing the name of the host on which this {@link Locator} is running.
* @param port {@link Integer} specifying the port number on which this {@link Locator} is listening.
*/
private Locator(String host, Integer port) {
this.host = host;
this.port = port;
}
/**
* Return the {@link String name} of the host on which this {@link Locator} is running.
*
* Defaults to {@literal localhost}.
*
* @return the {@link String name} of the host on which this {@link Locator} is running.
*/
public String getHost() {
return StringUtils.hasText(this.host) ? this.host : DEFAULT_LOCATOR_HOST;
}
/**
* Returns the {@link Integer port} on which this {@link Locator} is listening.
*
* Defaults to {@literal 10334}.
*
* @return the {@link Integer port} on which this {@link Locator} is listening.
*/
public int getPort() {
return this.port != null ? this.port : DEFAULT_LOCATOR_PORT;
}
@Override
public int compareTo(Locator other) {
int result = this.getHost().compareTo(other.getHost());
return result != 0 ? result : (this.getPort() - other.getPort());
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Locator)) {
return false;
}
Locator that = (Locator) obj;
return this.getHost().equals(that.getHost())
&& this.getPort() == that.getPort();
}
@Override
public int hashCode() {
int hashValue = 17;
hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(getHost());
hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(getPort());
return hashValue;
}
@Override
public String toString() {
return String.format("%s[%d]", getHost(), getPort());
}
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2017-present 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.geode.core.env.support;
import org.springframework.util.Assert;
/**
* The {@link Service} class is an Abstract Data Type (ADT) modeling a Pivotal CloudFoundry Service.
*
* @author John Blum
* @since 1.0.0
*/
@SuppressWarnings("unused")
public class Service {
/**
* Factory method to construct a new {@link Service} initialized with a {@link String name}.
*
* @param name {@link String} containing the name of the {@link Service}.
* @return a new {@link Service} configured with the given {@link String name}.
* @throws IllegalArgumentException if the {@link String name} is {@literal null} or empty.
* @see #Service(String)
*/
public static Service with(String name) {
return new Service(name);
}
private final String name;
/**
* Constructs a new {@link Service} initialized with a {@link String name}.
*
* @param name {@link String} containing the name of the {@link Service}.
* @throws IllegalArgumentException if the {@link String name} is {@literal null} or empty.
*/
Service(String name) {
Assert.hasText(name, String.format("Service name [%s] is required", name));
this.name = name;
}
/**
* Returns the {@link String name} of this {@link Service}.
*
* @return this {@link Service Service's} {@link String name}.
*/
public String getName() {
return this.name;
}
@Override
public String toString() {
return getName();
}
}

View File

@@ -0,0 +1,182 @@
/*
* Copyright 2017-present 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.geode.core.env.support;
import java.util.Arrays;
import java.util.Optional;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* The {@link User} class is an Abstract Data Type (ADT) modeling a user in Pivotal CloudFoundry (PCF).
*
* @author John Blum
* @since 1.0.0
*/
@SuppressWarnings("unused")
public class User implements Comparable<User> {
private Role role;
private final String name;
private String password;
/**
* Factory method used to construct a new {@link User} initialized with the given {@link String name}.
*
* @param name {@link String} containing the name of the {@link User}.
* @return a new {@link User} initialized witht he given {@link String name}.
* @throws IllegalArgumentException if {@link String name} is {@literal null} or empty.
* @see #User(String)
*/
public static User with(String name) {
return new User(name);
}
/**
* Constructs a new {@link User} initialized with the given {@link String name}.
*
* @param name {@link String} containing the name of the {@link User}.
* @throws IllegalArgumentException if {@link String name} is {@literal null} or empty.
*/
private User(String name) {
Assert.hasText(name, String.format("User name [%s] is required", name));
this.name = name;
}
/**
* Returns the {@link String name} of this {@link User}.
*
* @return a {@link String} containing the {@link User User's} name.
*/
public String getName() {
return this.name;
}
/**
* Returns an {@link Optional} {@link String} containing {@link User User's} password.
*
* @return an {@link Optional} {@link String} containing {@link User User's} password.
* @see java.util.Optional
*/
public Optional<String> getPassword() {
return Optional.ofNullable(this.password).filter(StringUtils::hasText);
}
/**
* Returns an {@link Optional} {@link Role} for this {@link User}.
*
* @return an {@link Optional} {@link Role} for this {@link User}.
* @see org.springframework.geode.core.env.support.User.Role
* @see java.util.Optional
*/
public Optional<Role> getRole() {
return Optional.ofNullable(this.role);
}
/**
* Builder method used to set this {@link User User's} {@link String password}.
*
* @param password {@link String} containing this {@link User User's} password.
* @return this {@link User}.
*/
public User withPassword(String password) {
this.password = password;
return this;
}
/**
* Builder method used to set this {@link User User's} {@link Role}.
*
* @param role assigned {@link Role} of this {@link User}.
* @return this {@link User}.
* @see org.springframework.geode.core.env.support.User
*/
public User withRole(Role role) {
this.role = role;
return this;
}
@Override
@SuppressWarnings("all")
public int compareTo(User other) {
return this.getName().compareTo(other.getName());
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof User)) {
return false;
}
User that = (User) obj;
return this.getName().equals(that.getName());
}
@Override
public int hashCode() {
int hashValue = 17;
hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(getName());
return hashValue;
}
@Override
public String toString() {
return getName();
}
public enum Role {
CLUSTER_OPERATOR,
DEVELOPER;
public static Role of(String name) {
return Arrays.stream(values())
.filter(role -> role.name().equalsIgnoreCase(String.valueOf(name).trim()))
.findFirst()
.orElse(null);
}
public boolean isClusterOperator() {
return CLUSTER_OPERATOR.equals(this);
}
public boolean isDeveloper() {
return DEVELOPER.equals(this);
}
@Override
public String toString() {
return name().toLowerCase();
}
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2017-present 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.geode.core.io;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
import org.springframework.core.io.Resource;
import org.springframework.geode.core.io.support.ResourceUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* Abstract base class providing functionality common to all {@link ResourceReader} implementations.
*
* @author John Blum
* @see java.io.InputStream
* @see org.springframework.core.io.Resource
* @see ResourceReader
* @since 1.3.1
*/
public abstract class AbstractResourceReader implements ResourceReader {
/**
* @inheritDoc
*/
@Override
public @NonNull byte[] read(@NonNull Resource resource) {
return Optional.ofNullable(resource)
.filter(this::isAbleToHandle)
.map(this::preProcess)
.map(it -> {
try (InputStream in = it.getInputStream()) {
return doRead(in);
}
catch (IOException cause) {
throw new ResourceReadException(String.format("Failed to read from Resource [%s]",
it.getDescription()), cause);
}
})
.orElseThrow(() -> new UnhandledResourceException(String.format("Unable to handle Resource [%s]",
ResourceUtils.nullSafeGetDescription(resource))));
}
/**
* Determines whether this reader is able to handle and read from the target {@link Resource}.
*
* The default implementation determines that the {@link Resource} can be handled if the {@link Resource} handle
* is not {@literal null}.
*
* @param resource {@link Resource} to evaluate.
* @return a boolean value indicating whether this reader is able to handle and read from
* the target {@link Resource}.
* @see org.springframework.core.io.Resource
*/
@SuppressWarnings("unused")
protected boolean isAbleToHandle(@Nullable Resource resource) {
return resource != null;
}
/**
* Reads data from the target {@link Resource} (intentionally) by using the {@link InputStream} returned by
* {@link Resource#getInputStream()}.
*
* However, other algorithm/strategy implementations are free to read from the {@link Resource} as is appropriate
* for the given context (e.g. cloud environment). In those cases, implementors should override
* the {@link #read(Resource)} method.
*
* @param resourceInputStream {@link InputStream} used to read data from the target {@link Resource}.
* @return a {@literal non-null} byte array containing the data from the target {@link Resource}.
* @throws IOException if an I/O error occurs while reading from the {@link Resource}.
* @see java.io.InputStream
* @see #read(Resource)
*/
protected abstract @NonNull byte[] doRead(@NonNull InputStream resourceInputStream) throws IOException;
/**
* Pre-processes the target {@link Resource} before reading from the {@link Resource}.
*
* @param resource {@link Resource} to pre-process; never {@literal null}.
* @return the given, target {@link Resource}.
* @see org.springframework.core.io.Resource
*/
protected @NonNull Resource preProcess(@NonNull Resource resource) {
return resource;
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2017-present 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.geode.core.io;
import java.io.IOException;
import java.io.OutputStream;
import org.springframework.core.io.Resource;
import org.springframework.core.io.WritableResource;
import org.springframework.geode.core.io.support.ResourceUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* Abstract base class providing functionality common to all {@link ResourceWriter} implementations.
*
* @author John Blum
* @see java.io.OutputStream
* @see org.springframework.core.io.Resource
* @see org.springframework.core.io.WritableResource
* @see ResourceWriter
* @since 1.3.1
*/
public abstract class AbstractResourceWriter implements ResourceWriter {
/**
* @inheritDoc
*/
@Override
public void write(@NonNull Resource resource, byte[] data) {
ResourceUtils.asWritableResource(resource)
.filter(this::isAbleToHandle)
.map(this::preProcess)
.map(it -> {
try (OutputStream out = it.getOutputStream()) {
doWrite(out, data);
return true;
}
catch (IOException cause) {
throw new ResourceWriteException(String.format("Failed to write to Resource [%s]",
it.getDescription()), cause);
}
})
.orElseThrow(() -> new UnhandledResourceException(String.format("Unable to handle Resource [%s]",
ResourceUtils.nullSafeGetDescription(resource))));
}
/**
* Determines whether this writer is able to handle and write to the target {@link Resource}.
*
* The default implementation determines that the {@link Resource} can be handled if the {@link Resource} handle
* is not {@literal null}.
*
* @param resource {@link Resource} to evaluate.
* @return a boolean value indicating whether this writer is able to handle and write to the target {@link Resource}.
* @see org.springframework.core.io.Resource
*/
@SuppressWarnings("unused")
protected boolean isAbleToHandle(@Nullable Resource resource) {
return resource != null;
}
/**
* Writes the given data to the target {@link Resource} (intentionally) by using the {@link OutputStream}
* returned by {@link WritableResource#getOutputStream()}.
*
* However, other algorithm/strategy implementations are free to write to the {@link Resource} as is appropriate
* for the given context (e.g. cloud environment). In those cases, implementors should override
* the {@link #write(Resource, byte[])} method.
*
* @param resourceOutputStream {@link OutputStream} returned from {@link WritableResource#getOutputStream()}
* used to write the given data to the locations identified by the target {@link Resource}.
* @param data array of bytes containing the data to write.
* @throws IOException if an I/O error occurs while writing to the target {@link Resource}.
* @see java.io.OutputStream
*/
protected abstract void doWrite(OutputStream resourceOutputStream, byte[] data) throws IOException;
/**
* Pre-processes the target {@link WritableResource} before writing to the {@link WritableResource}.
*
* @param resource {@link WritableResource} to pre-process; never {@literal null}.
* @return the given, target {@link WritableResource}.
* @see org.springframework.core.io.WritableResource
*/
protected @NonNull WritableResource preProcess(@NonNull WritableResource resource) {
return resource;
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2017-present 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.geode.core.io;
import org.springframework.core.io.Resource;
/**
* A Java {@link RuntimeException} indicating a problem accessing (e.g. reading/writing) the data
* of the target {@link Resource}.
*
* @author John Blum
* @see java.lang.RuntimeException
* @see org.springframework.core.io.Resource
* @since 1.3.1
*/
@SuppressWarnings("unused")
public class ResourceDataAccessException extends RuntimeException {
/**
* Constructs a new instance of {@link ResourceDataAccessException} with no {@link String message}
* or known {@link Throwable cause}.
*/
public ResourceDataAccessException() { }
/**
* Constructs a new instance of {@link ResourceDataAccessException} initialized with the given {@link String message}
* to describe the error.
*
* @param message {@link String} describing the {@link RuntimeException}.
*/
public ResourceDataAccessException(String message) {
super(message);
}
/**
* Constructs a new instance of {@link ResourceDataAccessException} initialized with the given {@link Throwable}
* signifying the underlying cause of this {@link RuntimeException}.
*
* @param cause {@link Throwable} signifying the underlying cause of this {@link RuntimeException}.
* @see java.lang.Throwable
*/
public ResourceDataAccessException(Throwable cause) {
super(cause);
}
/**
* Constructs a new instance of {@link ResourceDataAccessException} initialized with the given {@link String message}
* describing the error along with a {@link Throwable} signifying the underlying cause of this
* {@link RuntimeException}.
*
* @param message {@link String} describing the {@link RuntimeException}.
* @param cause {@link Throwable} signifying the underlying cause of this {@link RuntimeException}.
* @see java.lang.Throwable
*/
public ResourceDataAccessException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2017-present 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.geode.core.io;
import org.springframework.core.io.Resource;
/**
* {@link RuntimeException} indication that a {@link Resource} could not be found.
*
* @author John Blum
* @see java.lang.RuntimeException
* @see org.springframework.core.io.Resource
* @since 1.3.1
*/
@SuppressWarnings("unused")
public class ResourceNotFoundException extends RuntimeException {
/**
* Constructs a new instance of {@link ResourceNotFoundException} with no {@link String message}
* or known {@link Throwable cause}.
*/
public ResourceNotFoundException() { }
/**
* Constructs a new instance of {@link ResourceNotFoundException} initialized with the given {@link String message}
* to describe the error.
*
* @param message {@link String} describing the {@link RuntimeException}.
*/
public ResourceNotFoundException(String message) {
super(message);
}
/**
* Constructs a new instance of {@link ResourceNotFoundException} initialized with the given {@link Throwable}
* signifying the underlying cause of this {@link RuntimeException}.
*
* @param cause {@link Throwable} signifying the underlying cause of this {@link RuntimeException}.
* @see java.lang.Throwable
*/
public ResourceNotFoundException(Throwable cause) {
super(cause);
}
/**
* Constructs a new instance of {@link ResourceNotFoundException} initialized with the given {@link String message}
* describing the error along with a {@link Throwable} signifying the underlying cause of this
* {@link RuntimeException}.
*
* @param message {@link String} describing the {@link RuntimeException}.
* @param cause {@link Throwable} signifying the underlying cause of this {@link RuntimeException}.
* @see java.lang.Throwable
*/
public ResourceNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2017-present 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.geode.core.io;
import org.springframework.core.io.Resource;
/**
* A {@link ResourceDataAccessException} and Java {@link RuntimeException} indicating a problem
* while reading from the target {@link Resource}.
*
* @author John Blum
* @see java.lang.RuntimeException
* @see org.springframework.core.io.Resource
* @see org.springframework.geode.core.io.ResourceDataAccessException
* @since 1.3.1
*/
@SuppressWarnings("unused")
public class ResourceReadException extends ResourceDataAccessException {
/**
* Constructs a new instance of {@link ResourceReadException} with no {@link String message}
* or known {@link Throwable cause}.
*/
public ResourceReadException() { }
/**
* Constructs a new instance of {@link ResourceReadException} initialized with the given {@link String message}
* to describe the error.
*
* @param message {@link String} describing the {@link RuntimeException}.
*/
public ResourceReadException(String message) {
super(message);
}
/**
* Constructs a new instance of {@link ResourceReadException} initialized with the given {@link Throwable}
* signifying the underlying cause of this {@link RuntimeException}.
*
* @param cause {@link Throwable} signifying the underlying cause of this {@link RuntimeException}.
* @see java.lang.Throwable
*/
public ResourceReadException(Throwable cause) {
super(cause);
}
/**
* Constructs a new instance of {@link ResourceReadException} initialized with the given {@link String message}
* describing the error along with a {@link Throwable} signifying the underlying cause of this
* {@link RuntimeException}.
*
* @param message {@link String} describing the {@link RuntimeException}.
* @param cause {@link Throwable} signifying the underlying cause of this {@link RuntimeException}.
* @see java.lang.Throwable
*/
public ResourceReadException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2017-present 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.geode.core.io;
import java.nio.ByteBuffer;
import org.springframework.core.io.Resource;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* Interface (contract) for readers to define the algorithm and strategy for reading data from a {@link Resource},
* such as by using the {@link Resource Resource's} {@link Resource#getInputStream() InputStream}.
*
* @author John Blum
* @see java.nio.ByteBuffer
* @see org.springframework.core.io.Resource
* @since 1.3.1
*/
@FunctionalInterface
@SuppressWarnings("unused")
public interface ResourceReader {
/**
* Reads data from the {@literal non-null} {@link Resource} into a byte array.
*
* This method should throw an {@link UnhandledResourceException} if the algorithm and strategy used by this reader
* is not able to or capable of reading from the {@link Resource} at its location. This allows subsequent readers
* in a composition to possibly handle the {@link Resource}. Any other {@link Throwable} thrown by this {@code read}
* method will break the chain of read calls in the composition.
*
* @param resource {@link Resource} to read data from.
* @return a {@literal non-null} byte array containing the data from the {@link Resource}.
* @see org.springframework.core.io.Resource
*/
@NonNull byte[] read(@NonNull Resource resource);
/**
* Reads data from the {@literal non-null} {@link Resource} into a {@link ByteBuffer}.
*
* @param resource {@link Resource} to read data from.
* @return a {@literal non-null} {@link ByteBuffer} containing the data from the {@link Resource}.
* @see org.springframework.core.io.Resource
* @see java.nio.ByteBuffer
* @see #read(Resource)
*/
default @NonNull ByteBuffer readIntoByteBuffer(@NonNull Resource resource) {
return ByteBuffer.wrap(read(resource));
}
/**
* Composes this {@link ResourceReader} with the given {@link ResourceReader}
* using the {@literal Composite Software Design Pattern}.
*
* @param reader {@link ResourceReader} to compose with this reader.
* @return a composite {@link ResourceReader} composed of this {@link ResourceReader}
* and the given {@link ResourceReader}. If the given {@link ResourceReader} is {@literal null},
* then this {@link ResourceReader} is returned.
* @see <a href="https://en.wikipedia.org/wiki/Composite_pattern">Compsite Software Design Pattern</a>
* @see ResourceReader
*/
default @NonNull ResourceReader thenReadFrom(@Nullable ResourceReader reader) {
return reader == null ? this
: resource -> {
try {
return this.read(resource);
}
catch (UnhandledResourceException ignore) {
return reader.read(resource);
}
};
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2017-present 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.geode.core.io;
import java.util.Optional;
import org.springframework.core.io.Resource;
import org.springframework.lang.NonNull;
import org.springframework.util.ClassUtils;
/**
* Interface defining a contract encapsulating an algorithm/strategy for resolving {@link Resource Resources}.
*
* @author John Blum
* @see java.lang.FunctionalInterface
* @see org.springframework.core.io.Resource
* @since 1.3.1.
*/
@FunctionalInterface
public interface ResourceResolver {
/**
* Gets the {@link ClassLoader} used by this {@link ResourceResolver} to resolve {@literal classpath}
* {@link Resource Resources}.
*
* By default, this method will return a {@link ClassLoader} determined by {@link ClassUtils#getDefaultClassLoader()},
* which first tries to return the {@link Thread#getContextClassLoader()}, then {@link Class#getClassLoader()},
* and finally, {@link ClassLoader#getSystemClassLoader()}.
*
* @return an {@link Optional} {@link ClassLoader} used to resolve {@literal classpath} {@link Resource Resources}.
* @see org.springframework.util.ClassUtils#getDefaultClassLoader()
* @see java.lang.ClassLoader
* @see java.util.Optional
*/
default Optional<ClassLoader> getClassLoader() {
return Optional.ofNullable(ClassUtils.getDefaultClassLoader());
}
/**
* Tries to resolve a {@link Resource} handle from the given, {@literal non-null} {@link String location}
* (e.g. {@link String filesystem path}).
*
* @param location {@link String location} identifying the {@link Resource} to resolve;
* must not be {@literal null}.
* @return an {@link Optional} {@link Resource} handle for the given {@link String location}.
* @see org.springframework.core.io.Resource
* @see java.util.Optional
*/
Optional<Resource> resolve(@NonNull String location);
/**
* Returns a {@literal non-null}, {@literal existing} {@link Resource} handle resolved from the given,
* {@literal non-null} {@link String location} (e.g. {@link String filesystem path}).
*
* @param location {@link String location} identifying the {@link Resource} to resolve;
* must not be {@literal null}.
* @return a {@literal non-null}, {@literal existing} {@link Resource} handle for
* the resolved {@link String location}.
* @throws ResourceNotFoundException if a {@link Resource} cannot be resolved from the given {@link String location}.
* A {@link Resource} is unresolvable if the given {@link String location} does not exist (physically);
* see {@link Resource#exists()}.
* @see org.springframework.core.io.Resource
* @see #resolve(String)
*/
default @NonNull Resource require(@NonNull String location) {
return resolve(location)
.filter(Resource::exists)
.orElseThrow(() -> new ResourceNotFoundException(String.format("Resource [%s] does not exist", location)));
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2017-present 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.geode.core.io;
import org.springframework.core.io.Resource;
/**
* A {@link ResourceDataAccessException} and Java {@link RuntimeException} indicating a problem
* while writing to the target {@link Resource}.
*
* @author John Blum
* @see java.lang.RuntimeException
* @see org.springframework.core.io.Resource
* @see org.springframework.geode.core.io.ResourceDataAccessException
* @since 1.3.1
*/
@SuppressWarnings("unused")
public class ResourceWriteException extends ResourceDataAccessException {
/**
* Constructs a new instance of {@link ResourceWriteException} with no {@link String message}
* or known {@link Throwable cause}.
*/
public ResourceWriteException() { }
/**
* Constructs a new instance of {@link ResourceWriteException} initialized with the given {@link String message}
* to describe the error.
*
* @param message {@link String} describing the {@link RuntimeException}.
*/
public ResourceWriteException(String message) {
super(message);
}
/**
* Constructs a new instance of {@link ResourceWriteException} initialized with the given {@link Throwable}
* signifying the underlying cause of this {@link RuntimeException}.
*
* @param cause {@link Throwable} signifying the underlying cause of this {@link RuntimeException}.
* @see java.lang.Throwable
*/
public ResourceWriteException(Throwable cause) {
super(cause);
}
/**
* Constructs a new instance of {@link ResourceWriteException} initialized with the given {@link String message}
* describing the error along with a {@link Throwable} signifying the underlying cause of this
* {@link RuntimeException}.
*
* @param message {@link String} describing the {@link RuntimeException}.
* @param cause {@link Throwable} signifying the underlying cause of this {@link RuntimeException}.
* @see java.lang.Throwable
*/
public ResourceWriteException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2017-present 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.geode.core.io;
import java.nio.ByteBuffer;
import org.springframework.core.io.Resource;
import org.springframework.core.io.WritableResource;
import org.springframework.lang.NonNull;
/**
* Interface (contract) for writers to define the algorithm or strategy for writing data to a target {@link Resource},
* such as by using the {@link WritableResource WritableResource's}
* {@link WritableResource#getOutputStream()} OutputStream}.
*
* @author John Blum
* @see org.springframework.core.io.Resource
* @see org.springframework.core.io.WritableResource
* @since 1.3.1
*/
@FunctionalInterface
@SuppressWarnings("unused")
public interface ResourceWriter {
/**
* Writes data to the target {@link Resource} as defined by the algorithm/strategy of this writer.
*
* This method should throw an {@link UnhandledResourceException} if the algorithm or strategy used by this writer
* is not able to or capable of writing to the {@link Resource} at its location. This allows subsequent writers
* in a composition to possibly handle the {@link Resource}. Any other {@link Exception} thrown by this
* {@code write} method will break the chain of write calls in the composition.
*
* @param resource {@link Resource} to write data to.
* @param data array of bytes containing the data to write to the target {@link Resource}.
* @see org.springframework.core.io.Resource
*/
void write(@NonNull Resource resource, byte[] data);
/**
* Writes data contained in the {@link ByteBuffer} to the target {@link Resource} as defined by
* the algorithm/strategy of this writer.
*
* This method should throw an {@link UnhandledResourceException} if the algorithm or strategy used by this writer
* is not able to or capable of writing to the {@link Resource} at its location. This allows subsequent writers
* in a composition to possibly handle the {@link Resource}. Any other {@link Exception} thrown by this
* {@code write} method will break the chain of write calls in the composition.
*
* @param resource {@link Resource} to write data to.
* @param data {@link ByteBuffer} containing the data to write to the target {@link Resource}.
* @see org.springframework.core.io.Resource
* @see java.nio.ByteBuffer
* @see #write(Resource, byte[])
*/
default void write (@NonNull Resource resource, ByteBuffer data) {
write(resource, data.array());
}
/**
* Composes this {@link ResourceWriter} with the given {@link ResourceWriter}
* using the {@literal Composite Software Design Pattern}.
*
* @param writer {@link ResourceWriter} to compose with this writer.
* @return a composite {@link ResourceWriter} composed of this {@link ResourceWriter}
* and the given {@link ResourceWriter}. If the given {@link ResourceWriter} is {@literal null},
* then this {@link ResourceWriter} is returned.
* @see <a href="https://en.wikipedia.org/wiki/Composite_pattern">Compsite Software Design Pattern</a>
* @see ResourceWriter
*/
default ResourceWriter thenWriteTo(ResourceWriter writer) {
return writer == null ? this
: (resource, data) -> {
try {
this.write(resource, data);
}
catch (UnhandledResourceException ignore) {
writer.write(resource, data);
}
};
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2017-present 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.geode.core.io;
import org.springframework.core.io.Resource;
/**
* A {@link RuntimeException} indicating that a {@link Resource} was not properly handled during normal processing.
*
* @author John Blum
* @see java.lang.RuntimeException
* @see org.springframework.core.io.Resource
* @since 1.3.1
*/
@SuppressWarnings("unused")
public class UnhandledResourceException extends RuntimeException {
/**
* Constructs a new instance of {@link UnhandledResourceException} with no {@link String message}
* or no known {@link Throwable cause}.
*/
public UnhandledResourceException() { }
/**
* Constructs a new instance of {@link UnhandledResourceException} initialized with the given {@link String message}
* to describe the error.
*
* @param message {@link String} describing the {@link RuntimeException}.
*/
public UnhandledResourceException(String message) {
super(message);
}
/**
* Constructs a new instance of {@link UnhandledResourceException} initialized with the given {@link Throwable}
* signifying the underlying cause of this {@link RuntimeException}.
*
* @param cause {@link Throwable} signifying the underlying cause of this {@link RuntimeException}.
* @see java.lang.Throwable
*/
public UnhandledResourceException(Throwable cause) {
super(cause);
}
/**
* Constructs a new instance of {@link UnhandledResourceException} initialized with the given {@link String message}
* describing the error along with a {@link Throwable} signifying the underlying cause of this
* {@link RuntimeException}.
*
* @param message {@link String} describing the {@link RuntimeException}.
* @param cause {@link Throwable} signifying the underlying cause of this {@link RuntimeException}.
* @see java.lang.Throwable
*/
public UnhandledResourceException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2017-present 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.geode.core.io.support;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import org.springframework.core.io.Resource;
import org.springframework.geode.core.io.AbstractResourceReader;
import org.springframework.lang.NonNull;
/**
* A concrete {@link AbstractResourceReader} implementation that reads data from a target {@link Resource Resource's}
* {@link Resource#getInputStream() InputStream} into a byte array.
*
* @author John Blum
* @see java.io.InputStream
* @see java.io.ByteArrayOutputStream
* @see org.springframework.core.io.Resource
* @see org.springframework.geode.core.io.AbstractResourceReader
* @since 1.3.1
*/
@SuppressWarnings("unused")
public class ByteArrayResourceReader extends AbstractResourceReader {
protected static final int DEFAULT_BUFFER_SIZE = 32768;
/**
* Returns the required {@link Integer#TYPE buffer size} used to capture data from the target {@link Resource}
* in chunks.
*
* Subclasses are encouraged to override this method as necessary to tune the buffer size. By default, the
* buffer size is {@literal 32K} or {@literal 32768} bytes.
*
* @return the required {@link Integer#TYPE buffer size} to read from the {@link Resource} in chunks.
*/
protected int getBufferSize() {
return DEFAULT_BUFFER_SIZE;
}
/**
* @inheritDoc
*/
@Override
protected @NonNull byte[] doRead(@NonNull InputStream resourceInputStream) throws IOException {
try (ByteArrayOutputStream out = new ByteArrayOutputStream(resourceInputStream.available())) {
byte[] buffer = new byte[getBufferSize()];
for (int bytesRead = resourceInputStream.read(buffer); bytesRead != -1; bytesRead = resourceInputStream.read(buffer)) {
// using a buffer will trigger a flush() automatically in the OutputStream
out.write(buffer, 0, bytesRead);
}
out.flush();
return out.toByteArray();
}
}
}

View File

@@ -0,0 +1,200 @@
/*
* Copyright 2017-present 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.geode.core.io.support;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.StandardOpenOption;
import java.util.Optional;
import org.springframework.core.io.Resource;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.data.gemfire.util.ArrayUtils;
import org.springframework.geode.core.io.AbstractResourceWriter;
import org.springframework.geode.core.io.ResourceDataAccessException;
import org.springframework.geode.core.io.ResourceWriteException;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* {@link AbstractResourceWriter} implementation that writes data of a {@link File} based {@link Resource}.
*
* @author John Blum
* @see java.io.File
* @see java.io.OutputStream
* @see java.nio.file.Files
* @see java.nio.file.OpenOption
* @see org.springframework.core.io.Resource
* @see org.springframework.geode.core.io.AbstractResourceWriter
* @since 1.3.1
*/
@SuppressWarnings("unused")
public class FileResourceWriter extends AbstractResourceWriter {
protected static final boolean DEFAULT_APPEND_TO_FILE = false;
protected static final int DEFAULT_BUFFER_SIZE = 16384;
private final ThreadLocal<Resource> resource = new ThreadLocal<>();
/**
* @inheritDoc
*/
@Override
protected void doWrite(OutputStream resourceOutputStream, byte[] data) {
if (ResourceUtils.isNotEmpty(data)) {
int bufferSize = getBufferSize();
int length = Math.min(bufferSize, data.length);
int offset = 0;
try (OutputStream out = decorate(resourceOutputStream)) {
while (offset < data.length) {
out.write(data, offset, length);
offset += bufferSize;
length = Math.min(bufferSize, data.length - offset);
}
out.flush();
}
catch (IOException cause) {
String message = String.format("Failed to write data (%1$d byte(s)) to Resource using [%2$s]",
data.length, getClass().getName());
throw new ResourceWriteException(message, cause);
}
}
}
/**
* @inheritDoc
*/
@Override
protected boolean isAbleToHandle(@Nullable Resource resource) {
if (super.isAbleToHandle(resource) && resource.isFile()) {
this.resource.set(resource);
return true;
}
return false;
}
/**
* Returns the configured {@link Integer#TYPE buffer size} used by this writer to chunk the data written to
* the {@link File}.
* <p>
* Subclasses should override this method to tune the buffer size based on the context and requirements.
*
* @return the configured {@link Integer#TYPE buffer size}.
*/
protected int getBufferSize() {
return DEFAULT_BUFFER_SIZE;
}
/**
* Returns the configured {@link OpenOption OpenOptions} used to configure the stream writing to the {@link File}.
*
* By default, the {@link File} will be {@link StandardOpenOption#CREATE created},
* {@link StandardOpenOption#TRUNCATE_EXISTING truncated} and {@link StandardOpenOption#WRITE written} to.
*
* Subclasses should override this method to tune the {@link File} stream based on context and requirements.
*
* @return configured {@link OpenOption OpenOptions}.
* @see java.nio.file.OpenOption
*/
protected OpenOption[] getOpenOptions() {
return ArrayUtils.asArray(
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE
);
}
/**
* Returns an {@link Optional} reference to the target {@link Resource}.
*
* @return an {@link Optional} reference to the target {@link Resource}.
* @see org.springframework.core.io.Resource
* @see java.util.Optional
*/
protected Optional<Resource> getResource() {
return Optional.ofNullable(this.resource.get());
}
/**
* Decorates the given {@link OutputStream} by adding buffering capabilities.
*
* @param outputStream {@link OutputStream} to decorate.
* @return the decorated {@link OutputStream}.
* @see #newFileOutputStream()
* @see java.io.OutputStream
*/
protected @NonNull OutputStream decorate(@Nullable OutputStream outputStream) {
return outputStream instanceof BufferedOutputStream ? outputStream
: outputStream != null ? new BufferedOutputStream(outputStream, getBufferSize())
: newFileOutputStream();
}
/**
* Tries to construct a new {@link File} based {@link OutputStream} from the {@literal target} {@link Resource}.
*
* By default, the constructed {@link OutputStream} is also buffered (e.g. {@link BufferedOutputStream}).
*
* @return a {@link OutputStream} writing to a {@link File} identified by the {@literal target} {@link Resource}.
* @throws IllegalStateException if the {@literal target} {@link Resource} cannot be handled as a {@link File}.
* @throws DataAccessResourceFailureException if the {@link OutputStream} could not be created.
* @see java.io.BufferedOutputStream
* @see java.io.OutputStream
* @see #getBufferSize()
* @see #getOpenOptions()
* @see #getResource()
*/
protected OutputStream newFileOutputStream() {
return getResource()
.filter(this::isAbleToHandle)
.map(resource -> {
try {
OutputStream fileOutputStream =
Files.newOutputStream(resource.getFile().toPath(), getOpenOptions());
return new BufferedOutputStream(fileOutputStream, getBufferSize());
}
catch (IOException cause) {
String message = String.format("Failed to access the Resource [%s] as a file",
resource.getDescription());
throw new ResourceDataAccessException(message, cause);
}
})
.orElseThrow(() -> newIllegalStateException("Resource [%s] is not a file based resource",
getResource().map(Resource::getDescription).orElse(null)));
}
}

View File

@@ -0,0 +1,219 @@
/*
* Copyright 2017-present 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.geode.core.io.support;
import static org.springframework.geode.core.util.ObjectUtils.initialize;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.geode.core.io.ResourceNotFoundException;
import org.springframework.geode.core.io.ResourceResolver;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* {@link ResourceResolver} implementation using Spring's {@link ResourceLoader} to resolve
* and load {@link Resource Resources}.
*
* @author John Blum
* @see org.springframework.context.ApplicationContext
* @see org.springframework.context.ResourceLoaderAware
* @see org.springframework.core.io.ClassPathResource
* @see org.springframework.core.io.DefaultResourceLoader
* @see org.springframework.core.io.Resource
* @see org.springframework.core.io.ResourceLoader
* @see org.springframework.geode.core.io.ResourceResolver
* @since 1.3.1
*/
public class ResourceLoaderResourceResolver implements ResourceLoaderAware, ResourceResolver {
private final AtomicReference<ResourceLoader> resolvedResourceLoader = new AtomicReference<>(null);
/**
* Gets an {@link Optional} {@link ClassLoader} used by the {@link ResourceLoader} to resolve and load
* {@link Resource Resources} located on the {@literal classpath}.
*
* Returns the {@link ResourceLoader#getClassLoader() ClassLoader} from the configured {@link ResourceLoader},
* if present. Otherwise, returns a {@link ClassLoader} determined by {@link ClassUtils#getDefaultClassLoader()},
* which first tries to return the {@link Thread#getContextClassLoader()}, then {@link Class#getClassLoader()},
* and finally, {@link ClassLoader#getSystemClassLoader()}.
*
* @return an {@link Optional} {@link ClassLoader} to resolve and load {@link Resource Resources}.
* @see org.springframework.core.io.ResourceLoader#getClassLoader()
* @see java.lang.ClassLoader
* @see java.util.Optional
*/
@Override
public Optional<ClassLoader> getClassLoader() {
return Optional.ofNullable(Optional.ofNullable(this.resolvedResourceLoader.get())
.map(ResourceLoader::getClassLoader)
.orElseGet(ClassUtils::getDefaultClassLoader));
}
/**
* Configures the {@link ResourceLoader} used by this {@link ResourceResolver} to resolve and load
* {@link Resource Resources}.
*
* @param resourceLoader {@link ResourceLoader} used to resolve and load {@link Resource Resources}.
* @see org.springframework.core.io.ResourceLoader
*/
@Override
public void setResourceLoader(@Nullable ResourceLoader resourceLoader) {
this.resolvedResourceLoader.set(resourceLoader);
}
/**
* Returns a reference to the configured {@link ResourceLoader} used to load {@link Resource Resources}.
*
* If a {@link ResourceLoader} was not explicitly configured, then a {@literal default} {@link ResourceLoader}
* using a {@literal default} {@link ClassLoader} is provided.
*
* @return a reference to the configured {@link ResourceLoader}; never {@literal null}.
* @see org.springframework.core.io.ResourceLoader
* @see #newResourceLoader()
*/
protected @NonNull ResourceLoader getResourceLoader() {
return this.resolvedResourceLoader.updateAndGet(resourceLoader ->
initialize(resourceLoader, this::newResourceLoader));
}
/**
* Constructs a new, {@literal default} instance of {@link ResourceLoader} to load {@link Resource Resources}.
*
* Specifically, creates a standalone {@link DefaultResourceLoader} initialized with a {@literal default}
* {@link ClassLoader} as determined by {@link #getClassLoader()}.
*
* @return a new, {@literal default} instance of {@link ResourceLoader}.
* @see org.springframework.core.io.ResourceLoader
*/
protected @NonNull ResourceLoader newResourceLoader() {
return getClassLoader()
.map(DefaultResourceLoader::new)
.orElseGet(DefaultResourceLoader::new);
}
/**
* Constructs a new {@link Resource} handle at the given {@link String location}.
*
* By default, a {@link ClassPathResource} is constructed.
*
* @param location {@link String location} of the new {@link Resource}; must not be {@literal null}.
* @return a new {@link Resource} handle at the given {@link String location}.
* @throws IllegalArgumentException if {@link String location} is not specified.
* @see org.springframework.core.io.Resource
*/
protected @NonNull Resource newResource(@NonNull String location) {
Assert.hasText(location, () ->
String.format("The location [%s] of the Resource must be specified", location));
return new ClassPathResource(location);
}
/**
* Determines whether the {@link Resource} is a {@literal qualified} {@link Resource}.
*
* Qualifications are determined by the application Requirements and Use Case (UC) at time of resolution.
* For example, it maybe that the {@link Resource} must {@link Resource#exists() exist} to qualify, or that
* the {@link Resource} must have a valid protocol, path and name.
*
* This default implementation requires the target {@link Resource} to not be {@literal null}.
*
* @param resource {@link Resource} to qualify.
* @return a boolean value indicating whether the {@link Resource} is qualified.
* @see org.springframework.core.io.Resource
*/
protected boolean isQualified(@Nullable Resource resource) {
return resource != null;
}
/**
* Action to perform when the {@link Resource} identified at the specified {@link String location} is missing,
* or was not {@link #isQualified(Resource) qualified}.
*
* @param resource missing {@link Resource}.
* @param location {@link String} containing the location identifying the missing {@link Resource}.
* @throws ResourceNotFoundException if the {@link Resource} cannot be found at the specified {@link String location}.
* @return a different {@link Resource}, possibly. Alternatively, this method may throw
* a {@link ResourceNotFoundException}.
* @see #isQualified(Resource)
*/
protected @Nullable Resource onMissingResource(@Nullable Resource resource, @NonNull String location) {
throw new ResourceNotFoundException(String.format("Failed to resolve Resource [%1$s] at location [%2$s]",
ResourceUtils.nullSafeGetDescription(resource), location));
}
/**
* Method used by subclasses to process the loaded {@link Resource} as determined by
* the {@link #getResourceLoader() ResourceLoader}.
*
* @param resource {@link Resource} to post-process.
* @return the {@link Resource}.
* @see org.springframework.core.io.Resource
*/
protected Resource postProcess(Resource resource) {
return resource;
}
/**
* Tries to resolve a {@link Resource} at the given {@link String location} using a Spring {@link ResourceLoader},
* such as a Spring {@link ApplicationContext}.
*
* The targeted, identified {@link Resource} can be further {@link #isQualified(Resource) qualified} by subclasses
* based on application requirements or use case (UC).
*
* In the event that a {@link Resource} cannot be identified at the given {@link String location}, then applications
* have 1 last opportunity to handle the missing {@link Resource} event, and either return a different or default
* {@link Resource} or throw a {@link ResourceNotFoundException}.
*
* @param location {@link String location} identifying the {@link Resource} to resolve;
* must not be {@literal null}.
* @throws IllegalArgumentException if {@link String location} is not specified.
* @return an {@link Optional} {@link Resource} handle for the given {@link String location}.
* @see org.springframework.core.io.Resource
* @see #isQualified(Resource)
* @see #onMissingResource(Resource, String)
* @see #postProcess(Resource)
* @see java.util.Optional
*/
@Override
public Optional<Resource> resolve(@NonNull String location) {
Assert.hasText(location, () ->
String.format("The location [%s] of the Resource to resolve must be specified", location));
Resource resource = getResourceLoader().getResource(location);
resource = postProcess(resource);
Resource resolvedResource = isQualified(resource)
? resource
: onMissingResource(resource, location);
return Optional.ofNullable(resolvedResource);
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2017-present 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.geode.core.io.support;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* An enumeration of {@link Resource} {@link String prefixes} recognized by the Spring Framework.
*
* @author John Blum
* @see org.springframework.core.io.Resource
* @since 1.3.1
*/
public enum ResourcePrefix {
CLASSPATH_URL_PREFIX(ResourceLoader.CLASSPATH_URL_PREFIX),
FILESYSTEM_URL_PREFIX("file:"),
HTTP_URL_PREFIX("http:");
public static final String RESOURCE_PATH_SEPARATOR = "/";
/**
* Factory method used to try and find a {@link ResourcePrefix} enumerated value
* matching the given {@link String prefix}.
*
* @param prefix {@link String} with the name of the prefix.
* @return a {@link ResourcePrefix} matching the given {@link String prefix} by name, or {@literal null}
* if the {@link String prefix} does not match any {@link ResourcePrefix} enumerated value.
*/
public static @Nullable ResourcePrefix from(@Nullable String prefix) {
if (StringUtils.hasText(prefix)) {
prefix = prefix.trim().toLowerCase();
for (ResourcePrefix resourcePrefix : values()) {
if (resourcePrefix.toString().equals(prefix)) {
return resourcePrefix;
}
}
}
return null;
}
private final String prefix;
/**
* Constructs a new instance of {@link ResourcePrefix} initialized with the named {@link String prefix}.
*
* @param prefix {@link String name} of the prefix.
* @throws IllegalArgumentException if the {@link String prefix} is not specified.
*/
ResourcePrefix(String prefix) {
Assert.hasText(prefix, "Resource prefix must be specified");
this.prefix = prefix;
}
/**
* Gets the network protocol that this {@link ResourcePrefix} represents.
*
* @return the network protocol that this {@link ResourcePrefix} represents.
*/
public String getProtocol() {
StringBuilder buffer = new StringBuilder();
for (char character : this.prefix.toCharArray()) {
if (Character.isAlphabetic(character)) {
buffer.append(character);
}
}
return buffer.toString();
}
/**
* Gets the {@link String pattern} or template used to construct a {@link java.net.URL} prefix
* from this {@link ResourcePrefix}.
*
* @return the {@link String pattern} or template used to construct a {@link java.net.URL} prefix
* from this {@link ResourcePrefix}.
* @see #toUrlPrefix()
*/
protected String getUrlPrefixPattern() {
return this.equals(CLASSPATH_URL_PREFIX) ? "%s" : "%1$s%2$s%2$s";
}
@Override
public String toString() {
return this.prefix;
}
/**
* Gets the {@link ResourcePrefix} as a prefix use in a {@link java.net.URL}.
*
* @return the {@link java.net.URL} prefix.
* @see #getUrlPrefixPattern()
*/
public String toUrlPrefix() {
return String.format(getUrlPrefixPattern(), toString(), RESOURCE_PATH_SEPARATOR);
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright 2017-present 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.geode.core.io.support;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException;
import java.util.Optional;
import org.springframework.core.io.Resource;
import org.springframework.core.io.WritableResource;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* Abstract utility class containing functionality to work with {@link Resource Resources}.
*
* @author John Blum
* @see org.springframework.core.io.Resource
* @see org.springframework.core.io.WritableResource
* @since 1.3.1
*/
public abstract class ResourceUtils {
/**
* Returns the {@link Resource} as a {@link WritableResource} if possible.
*
* This method makes a best effort to determine whether the target {@link Resource} is actually {@literal writable}.
* Even still, it may be possible that a write to the target {@link Resource} will fail.
*
* The {@link Resource} is {@literal writable} if the {@link Resource} is an instance of {@link WritableResource}
* and {@link WritableResource#isWritable()} returns {@literal true}.
*
* @param resource {@link Resource} to cast to a {@link WritableResource}.
* @return a {@link WritableResource} from the target {@link Resource} if possible; never {@literal null}.
* @throws IllegalStateException if the target {@link Resource} is not {@literal writable}.
* @see org.springframework.core.io.WritableResource
* @see org.springframework.core.io.Resource
*/
public static @NonNull WritableResource asStrictlyWritableResource(@Nullable Resource resource) {
return Optional.ofNullable(resource)
.filter(WritableResource.class::isInstance)
.map(WritableResource.class::cast)
.filter(WritableResource::isWritable)
.orElseThrow(() -> newIllegalStateException("Resource [%s] is not writable",
ResourceUtils.nullSafeGetDescription(resource)));
}
/**
* {@link Optional Optionally} return the {@link Resource} as a {@link WritableResource}.
*
* The {@link Resource} must be an instance of {@link WritableResource}.
*
* @param resource {@link Resource} to cast to a {@link WritableResource}.
* @return the {@link Resource} as a {@link WritableResource} if the {@link Resource}
* is an instance of {@link WritableResource}, otherwise returns {@link Optional#empty()}.
* @see org.springframework.core.io.WritableResource
* @see org.springframework.core.io.Resource
* @see java.util.Optional
*/
public static Optional<WritableResource> asWritableResource(@Nullable Resource resource) {
return Optional.ofNullable(resource)
.filter(WritableResource.class::isInstance)
.map(WritableResource.class::cast);
}
/**
* Determines whether the given byte array is {@literal null} or {@literal empty}.
*
* @param array byte array to evaluate.
* @return a boolean value indicating whether the given byte array is {@literal null} or {@literal empty}.
*/
public static boolean isNotEmpty(@Nullable byte[] array) {
return array != null && array.length > 0;
}
/**
* Null-safe operation to determine whether the given {@link Resource} is readable.
*
* @param resource {@link Resource} to evaluate.
* @return a boolean value indicating whether the given {@link Resource} is readable.
* @see org.springframework.core.io.Resource#isReadable()
* @see org.springframework.core.io.Resource
*/
public static boolean isReadable(@Nullable Resource resource) {
return resource != null && resource.isReadable();
}
/**
* Null-safe operation to determine whether the given {@link Resource} is writable.
*
* @param resource {@link Resource} to evaluate.
* @return a boolean value indicating whether the given {@link Resource} is writable.
* @see org.springframework.core.io.WritableResource#isWritable()
* @see org.springframework.core.io.WritableResource
* @see org.springframework.core.io.Resource
*/
public static boolean isWritable(@Nullable Resource resource) {
return resource instanceof WritableResource && ((WritableResource) resource).isWritable();
}
/**
* Null-safe method to get the {@link Resource#getDescription() description} of the given {@link Resource}.
*
* @param resource {@link Resource} to describe.
* @return a {@link Resource#getDescription() description} of the {@link Resource}, or {@literal null}
* if the {@link Resource} handle is {@literal null}.
* @see org.springframework.core.io.Resource
*/
public static @Nullable String nullSafeGetDescription(@Nullable Resource resource) {
return resource != null ? resource.getDescription() : null;
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2017-present 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.geode.core.io.support;
import java.util.Optional;
import org.springframework.core.io.Resource;
import org.springframework.geode.core.io.ResourceResolver;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* {@link ResourceResolver} that returns a single (i.e. {@literal Singleton}) {@link Resource}
* regardless of {@link String location}.
*
* @author John Blum
* @see org.springframework.core.io.Resource
* @see org.springframework.geode.core.io.ResourceResolver
* @since 1.3.1
*/
public class SingleResourceResolver implements ResourceResolver {
@Nullable
private final Resource resource;
/**
* Constructs a new instance of {@link SingleResourceResolver} initialized with the given {@link Resource}.
*
* @param resource the {@literal single} {@link Resource} consistently resolved by this resolver.
* @see org.springframework.core.io.Resource
*/
public SingleResourceResolver(@Nullable Resource resource) {
this.resource = resource;
}
/**
* @inheritDoc
*/
@Override
public Optional<Resource> resolve(@NonNull String location) {
return Optional.ofNullable(this.resource);
}
}

View File

@@ -0,0 +1,226 @@
/*
* Copyright 2017-present 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.geode.core.util;
import java.util.function.Consumer;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* Abstract utility class used to process {@link Object managed object} {@literal aware} {@link Object objects},
* such as {@link ApplicationContextAware} {@link Object objects} in a Spring context.
*
* @author John Blum
* @see java.util.function.Consumer
* @see org.springframework.beans.factory.BeanClassLoaderAware
* @see org.springframework.beans.factory.BeanFactoryAware
* @see org.springframework.beans.factory.BeanNameAware
* @see org.springframework.context.ApplicationContextAware
* @see org.springframework.context.ApplicationEventPublisherAware
* @see org.springframework.context.EnvironmentAware
* @see org.springframework.context.ResourceLoaderAware
* @since 1.3.1
*/
public abstract class ObjectAwareUtils {
public static final Consumer<Object> NO_OP = target -> { };
/**
* Returns a {@link Consumer} capable of initializing an {@link ApplicationContextAware} {@link Object}
* with the given {@link ApplicationContext}.
*
* The {@link ApplicationContextAware#setApplicationContext(ApplicationContext)} method is only called on
* the {@link ApplicationContextAware} {@link Object} if the {@link ApplicationContext} is not {@literal null}.
*
* @param applicationContext {@link ApplicationContext} set on the {@link ApplicationContextAware} {@link Object}
* by the {@link Consumer}.
* @return a {@link Consumer} capable of initializing an {@link ApplicationContextAware} {@link Object}
* with the given {@link ApplicationContext}; never {@literal null}.
* @see org.springframework.context.ApplicationContextAware
* @see org.springframework.context.ApplicationContext
* @see java.util.function.Consumer
*/
public static @NonNull Consumer<Object> applicationContextAwareObjectInitializer(
@Nullable ApplicationContext applicationContext) {
return applicationContext == null ? NO_OP : target -> {
if (target instanceof ApplicationContextAware) {
((ApplicationContextAware) target).setApplicationContext(applicationContext);
}
};
}
/**
* Returns a {@link Consumer} capable of initializing an {@link ApplicationEventPublisherAware} {@link Object}
* with the given {@link ApplicationEventPublisher}.
*
* The {@link ApplicationEventPublisherAware#setApplicationEventPublisher(ApplicationEventPublisher)} method is only
* called on the {@link ApplicationEventPublisherAware} {@link Object} if the {@link ApplicationEventPublisherAware}
* is not {@literal null}.
*
* @param applicationEventPublisher {@link ApplicationEventPublisher} set on
* the {@link ApplicationEventPublisherAware} {@link Object} by the {@link Consumer}.
* @return a {@link Consumer} capable of initializing an {@link ApplicationEventPublisherAware} {@link Object}
* with the given {@link ApplicationEventPublisher}; never {@literal null}.
* @see org.springframework.context.ApplicationEventPublisherAware
* @see org.springframework.context.ApplicationEventPublisher
* @see java.util.function.Consumer
*/
public static @NonNull Consumer<Object> applicationEventPublisherAwareObjectInitializer(
@Nullable ApplicationEventPublisher applicationEventPublisher) {
return applicationEventPublisher == null ? NO_OP : target -> {
if (target instanceof ApplicationEventPublisherAware) {
((ApplicationEventPublisherAware) target).setApplicationEventPublisher(applicationEventPublisher);
}
};
}
/**
* Returns a {@link Consumer} capable of initializing an {@link BeanClassLoaderAware} {@link Object}
* with the given bean {@link ClassLoader}.
*
* The {@link BeanClassLoaderAware#setBeanClassLoader(ClassLoader)} method is only called on
* the {@link BeanClassLoaderAware} {@link Object} if the {@link ClassLoader} is not {@literal null}.
*
* @param beanClassLoader {@link ClassLoader} set on the {@link BeanClassLoaderAware} {@link Object}
* by the {@link Consumer}.
* @return a {@link Consumer} capable of initializing an {@link BeanClassLoaderAware} {@link Object}
* with the given bean {@link ClassLoader}; never {@literal null}.
* @see org.springframework.beans.factory.BeanClassLoaderAware
* @see java.util.function.Consumer
* @see java.lang.ClassLoader
*/
public static @NonNull Consumer<Object> beanClassLoaderAwareObjectInitializer(@Nullable ClassLoader beanClassLoader) {
return beanClassLoader == null ? NO_OP : target -> {
if (target instanceof BeanClassLoaderAware) {
((BeanClassLoaderAware) target).setBeanClassLoader(beanClassLoader);
}
};
}
/**
* Returns a {@link Consumer} capable of initializing an {@link BeanFactoryAware} {@link Object}
* with the given {@link BeanFactory}.
*
* The {@link BeanFactoryAware#setBeanFactory(BeanFactory)} method is only called on the {@link BeanFactoryAware}
* {@link Object} if the {@link BeanFactory} is not {@literal null}.
*
* @param beanFactory {@link BeanFactory} set on the {@link BeanFactoryAware} {@link Object} by the {@link Consumer}.
* @return a {@link Consumer} capable of initializing an {@link BeanFactoryAware} {@link Object}
* with the given {@link BeanFactory}; never {@literal null}.
* @see org.springframework.beans.factory.BeanFactoryAware
* @see org.springframework.beans.factory.BeanFactory
* @see java.util.function.Consumer
*/
public static @NonNull Consumer<Object> beanFactoryAwareObjectInitializer(@Nullable BeanFactory beanFactory) {
return beanFactory == null ? NO_OP : target -> {
if (target instanceof BeanFactoryAware) {
((BeanFactoryAware) target).setBeanFactory(beanFactory);
}
};
}
/**
* Returns a {@link Consumer} capable of initializing an {@link BeanNameAware} {@link Object}
* with the given {@link String bean name}.
*
* The {@link BeanNameAware#setBeanName(String)} method is only called on the {@link BeanNameAware} {@link Object}
* if the {@link String bean name} is not {@literal null}.
*
* @param beanName {@link String} containing the bean name to set on the {@link BeanNameAware} {@link Object}
* by the {@link Consumer}.
* @return a {@link Consumer} capable of initializing an {@link BeanNameAware} {@link Object}
* with the given {@link String bean name}; never {@literal null}.
* @see org.springframework.beans.factory.BeanNameAware
* @see java.util.function.Consumer
*/
public static @NonNull Consumer<Object> beanNameAwareObjectInitializer(@Nullable String beanName) {
return hasNoText(beanName) ? NO_OP : target -> {
if (target instanceof BeanNameAware) {
((BeanNameAware) target).setBeanName(beanName);
}
};
}
/**
* Returns a {@link Consumer} capable of initializing an {@link EnvironmentAware} {@link Object}
* with the given {@link Environment}.
*
* The {@link EnvironmentAware#setEnvironment(Environment)} method is only called on the {@link EnvironmentAware}
* {@link Object} if the {@link Environment} is not {@literal null}.
*
* @param environment {@link Environment} set on the {@link EnvironmentAware} {@link Object} by the {@link Consumer}.
* @return a {@link Consumer} capable of initializing an {@link EnvironmentAware} {@link Object}
* with the given {@link Environment}; never {@literal null}.
* @see org.springframework.context.EnvironmentAware
* @see org.springframework.core.env.Environment
* @see java.util.function.Consumer
*/
public static @NonNull Consumer<Object> environmentAwareObjectInitializer(@Nullable Environment environment) {
return environment == null ? NO_OP : target -> {
if (target instanceof EnvironmentAware) {
((EnvironmentAware) target).setEnvironment(environment);
}
};
}
/**
* Returns a {@link Consumer} capable of initializing an {@link ResourceLoaderAware} {@link Object}
* with the given {@link ResourceLoader}.
*
* The {@link ResourceLoaderAware#setResourceLoader(ResourceLoader)} method is only called on
* the {@link ResourceLoaderAware} {@link Object} if the {@link ResourceLoader} is not {@literal null}.
*
* @param resourceLoader {@link ResourceLoader} set on the {@link ResourceLoaderAware} {@link Object}
* by the {@link Consumer}.
* @return a {@link Consumer} capable of initializing an {@link ResourceLoaderAware} {@link Object}
* with the given {@link ResourceLoader}; never {@literal null}.
* @see org.springframework.context.ResourceLoaderAware
* @see org.springframework.core.io.ResourceLoader
* @see java.util.function.Consumer
*/
public static @NonNull Consumer<Object> resourceLoaderAwareObjectInitializer(@Nullable ResourceLoader resourceLoader) {
return resourceLoader == null ? NO_OP : target -> {
if (target instanceof ResourceLoaderAware) {
((ResourceLoaderAware) target).setResourceLoader(resourceLoader);
}
};
}
private static boolean hasNoText(@Nullable String value) {
return !StringUtils.hasText(value);
}
}

View File

@@ -0,0 +1,437 @@
/*
* Copyright 2017-present 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.geode.core.util;
import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalArgumentException;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.apache.geode.pdx.PdxInstance;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ObjectUtils} class is an abstract utility class with operations for {@link Object objects}.
*
* @author John Blum
* @see java.lang.Object
* @see java.lang.reflect.Constructor
* @see java.lang.reflect.Field
* @see java.lang.reflect.Method
* @see org.apache.geode.pdx.PdxInstance
* @since 1.0.0
*/
@SuppressWarnings("unused")
public abstract class ObjectUtils extends org.springframework.util.ObjectUtils {
private static final Logger logger = LoggerFactory.getLogger(ObjectUtils.class);
/**
* Tries to cast the given source {@link Object} into an instance of the given {@link Class} type.
*
* This method is cable of handling Apache Geode {@link PdxInstance} types.
*
* @param <T> desired {@link Class type} of the source {@link Object}.
* @param source {@link Object} to evaluate.
* @param type desired target {@link Class} type; must not be {@literal null}.
* @return the source {@link Object} cast to an instance of the given {@link Class} type.
* @throws IllegalArgumentException if the source {@link Object} is not an instance of
* the given {@link Class} type or the {@link Class} type is {@literal null}.
* @see org.apache.geode.pdx.PdxInstance
* @see java.lang.Class
* @see java.lang.Object
*/
public static @Nullable <T> T asType(@Nullable Object source, @NonNull Class<T> type) {
Assert.notNull(type, "Class type must not be null");
Object target = source instanceof PdxInstance
? ((PdxInstance) source).getObject()
: source;
return target == null ? null
: Optional.of(target)
.filter(type::isInstance)
.map(type::cast)
.orElseThrow(() -> newIllegalArgumentException("Object [%s] is not an instance of type [%s]",
nullSafeClassName(target), type.getName()));
}
/**
* Safely executes the given {@link ExceptionThrowingOperation} handling any checked {@link Exception}
* thrown during the normal execution of the operation by rethrowing an {@link IllegalStateException}
* wrapping the original checked {@link Exception}.
*
* @param <T> {@link Class type} of {@link Object value} returned from the execution of the operation.
* @param operation {@link ExceptionThrowingOperation} to execute; must not be {@literal null}.
* @return the result of the {@link ExceptionThrowingOperation}.
* @throws IllegalStateException wrapping any checked {@link Exception} thrown by the operation.
* @see org.springframework.geode.core.util.ObjectUtils.ExceptionThrowingOperation
* @see #doOperationSafely(ExceptionThrowingOperation, Object)
*/
@Nullable
public static <T> T doOperationSafely(@NonNull ExceptionThrowingOperation<T> operation) {
return doOperationSafely(operation, (T) null);
}
/**
* Safely executes the given {@link ExceptionThrowingOperation} handling any checked {@link Exception}
* thrown during the normal execution of the operation by returning the given {@link Object default value}
* or throwing an {@link IllegalStateException} if the {@link Object default value} is {@literal null}.
*
* @param <T> {@link Class type} of {@link Object value} returned from the execution of the operation
* as well as the {@link Class type} of the {@link Object default value}.
* @param operation {@link ExceptionThrowingOperation} to execute; must not be {@literal null}.
* @param defaultValue {@link Object value} to return if the execution of the operation
* results in a checked {@link Exception}.
* @return the result of the {@link ExceptionThrowingOperation}, returning the {@link Object default value}
* if the execution of the operation throws a checked {@link Exception}
* @throws IllegalStateException wrapping any checked {@link Exception} thrown by the operation
* when the {@link Object default value} is {@literal null}.
* @see org.springframework.geode.core.util.ObjectUtils.ExceptionThrowingOperation
* @see #doOperationSafely(ExceptionThrowingOperation, Supplier)
* @see #returnValueThrowOnNull(Object, RuntimeException)
*/
@Nullable
public static <T> T doOperationSafely(@NonNull ExceptionThrowingOperation<T> operation, @NonNull T defaultValue) {
Supplier<T> valueSupplier = () -> defaultValue;
return doOperationSafely(operation, valueSupplier);
}
/**
* Safely executes the given {@link ExceptionThrowingOperation} handling any checked {@link Exception}
* thrown during the normal execution of the operation by returning a {@link Object default value}
* supplied by the given {@link Supplier}, or throws an {@link IllegalStateException}
* if the {@link Supplier supplied value} is {@literal null}.
*
* @param <T> {@link Class type} of {@link Object value} returned from the execution of the operation
* as well as the {@link Class type} of the {@link Object default value}.
* @param operation {@link ExceptionThrowingOperation} to execute; must not be {@literal null}.
* @param valueSupplier {@link Supplier} of the {@link Object value} to return if the execution of the operation
* results in a checked {@link Exception}; must not be {@literal null}.
* @return the result of the {@link ExceptionThrowingOperation}, returning the {@link Supplier supplied value}
* if the execution of the operation throws a checked {@link Exception}
* @throws IllegalStateException wrapping any checked {@link Exception} thrown by the operation
* when the {@link Supplier supplied value} is {@literal null}.
* @see org.springframework.geode.core.util.ObjectUtils.ExceptionThrowingOperation
* @see #doOperationSafely(ExceptionThrowingOperation, Function)
* @see #returnValueThrowOnNull(Object, RuntimeException)
* @see java.util.function.Supplier
*/
public static <T> T doOperationSafely(@NonNull ExceptionThrowingOperation<T> operation,
@NonNull Supplier<T> valueSupplier) {
Function<Throwable, T> exceptionHandlingFunction = cause ->
returnValueThrowOnNull(valueSupplier.get(), newIllegalStateException(cause, "Failed to execute operation"));
return doOperationSafely(operation, exceptionHandlingFunction);
}
/**
* Safely executes the given {@link ExceptionThrowingOperation} handling any checked {@link Exception}
* thrown during the normal execution of the operation by invoking the provided {@link Exception}
* handling {@link Function}.
*
* @param <T> {@link Class type} of {@link Object value} returned from the execution of the operation.
* @param operation {@link ExceptionThrowingOperation} to execute; must not be {@literal null}.
* @param exceptionHandlingFunction {@link Function} used to handle any checked {@link Exception}
* thrown by {@link ExceptionThrowingOperation}; must not be {@literal null}.
* @return the result of the {@link ExceptionThrowingOperation}.
* @see org.springframework.geode.core.util.ObjectUtils.ExceptionThrowingOperation
* @see java.util.function.Function
* @see java.lang.Throwable
*/
public static <T> T doOperationSafely(@NonNull ExceptionThrowingOperation<T> operation,
@NonNull Function<Throwable, T> exceptionHandlingFunction) {
try {
return operation.run();
}
catch (Exception cause) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("Failed to execute operation [%s]", operation), cause);
}
return exceptionHandlingFunction.apply(cause);
}
}
/**
* Finds a {@link Method} with the given {@link Method#getName() name} on {@link Class type} which can be invoked
* with the given {@link Object arguments}.
*
* @param type {@link Class} type to evaluate for the {@link Method}.
* @param methodName {@link String} containing the name of the {@link Method} to find.
* @param args {@link Object array of arguments} used when invoking the method.
* @return an {@link Optional} {@link Method} on {@link Class type} potentially matching
* the {@link Object arguments} of the invocation.
* @see java.lang.Class
* @see java.lang.reflect.Method
* @see java.util.Optional
*/
public static Optional<Method> findMethod(@NonNull Class<?> type, @NonNull String methodName, Object... args) {
return Arrays.stream(nullSafeArray(type.getDeclaredMethods(), Method.class))
.filter(methodNameMatchesPredicate(methodName))
.filter(argumentsMatchParameterTypesPredicate(args))
.findFirst();
}
private static Predicate<Method> argumentsMatchParameterTypesPredicate(Object... args) {
return method -> {
Class<?>[] parameterTypes = nullSafeArray(method.getParameterTypes(), Class.class);
Object[] arguments = nullSafeArray(args, Object.class);
if (arguments.length != parameterTypes.length) {
return false;
}
for (int index = 0; index < parameterTypes.length; index++) {
Object argument = arguments[index];
if (argument != null && !parameterTypes[index].isInstance(argument)) {
return false;
}
}
return true;
};
}
private static Predicate<Method> methodNameMatchesPredicate(String methodName) {
return method -> method.getName().equals(methodName);
}
/**
* Gets the {@link Object value} of the given {@link String named} {@link Field} on the given {@link Object}.
*
* @param <T> {@link Class type} of the {@link Field Field's} value.
* @param obj {@link Object} containing the {@link String named} {@link Field}.
* @param fieldName {@link String} containing the name of the {@link Field}.
* @return the {@link Object value} of the {@link String named} {@link Field} on the given {@link Object}.
* @throws IllegalArgumentException if {@link Object} is {@literal null}, the {@link String named} {@link Field}
* is not specified or the given {@link Object} contains no {@link Field} with the given {@link String name}.
* @see #get(Object, Field)
* @see java.lang.Object
*/
public static <T> T get(Object obj, String fieldName) {
Assert.notNull(obj, "Object is required");
Assert.hasText(fieldName, String.format("Field name [%s] is required", fieldName));
Field field = ReflectionUtils.findField(obj.getClass(), fieldName);
if (field != null) {
field = makeAccessible(field);
return get(obj, field);
}
throw newIllegalArgumentException("No field with name [%s] exists on object of type [%s]",
fieldName, ObjectUtils.nullSafeClassName(obj));
}
/**
* Gets the {@link Object value} of the given {@link Field} on the given {@link Object}.
*
* @param <T> {@link Class type} of the {@link Field Field's} value.
* @param obj {@link Object} containing the {@link Field}.
* @param field {@link Field} of the given {@link Object}.
* @return the {@link Object value} of the {@link Field} on the given {@link Object}.
* @throws IllegalArgumentException if {@link Object} or {@link Field} is {@literal null}.
* @see java.lang.reflect.Field
* @see java.lang.Object
*/
@SuppressWarnings("unchecked")
public static <T> T get(Object obj, Field field) {
Assert.notNull(obj, "Object is required");
Assert.notNull(field, "Field is required");
return doOperationSafely(() -> (T) field.get(obj), (T) null);
}
/**
* An initialization operator used to evalutate a given {@link Object target} and conditionally
* {@link Supplier supply} a new value if the {@link Object target} is {@literal null}.
*
* The {@code initialize} operator simplifies a common initialization safety pattern that appears in code as:
*
* <code>
* target = target != null ? target : new Target();
* </code>
*
* While the expression uses Java's ternary operator, users could very well use a if-then-else statement instead.
* Either way, since Java is {@literal call-by-value} then the above statement and expression can be replaced with:
*
* <code>
* target = initialize(target, Target::new);
* </code>
*
* @param <T> {@link Class type} of the {@link Object target}.
* @param target {@link Object} to evaluate and initialize; must not be {@literal null}.
* @param supplier {@link Supplier} used to initialize the {@link Object target} on return.
* @return the existing {@link Object target} if not {@literal null}, otherwise invoke the {@link Supplier}
* to supply a new instance of {@code T}.
*/
public static <T> T initialize(@Nullable T target, @NonNull Supplier<T> supplier) {
return target != null ? target : supplier.get();
}
/**
* Invokes a {@link Method} on an {@link Object} with the given {@link String name}.
*
* @param <T> {@link Class type} of the {@link Method} return value.
* @param obj {@link Object} on which to invoke the {@link Method}.
* @param methodName {@link String} containing the name of the {@link Method} to invoke on {@link Object}.
* @return the return value of the invoked {@link Method} on {@link Object}.
* @throws IllegalArgumentException if no {@link Method} with {@link String name} could be found on {@link Object}.
* @see java.lang.reflect.Method
* @see java.lang.Object
*/
@SuppressWarnings("unchecked")
public static <T> T invoke(Object obj, String methodName) {
return (T) Optional.ofNullable(obj)
.map(Object::getClass)
.map(type -> ReflectionUtils.findMethod(type, methodName))
.map(ObjectUtils::makeAccessible)
.map(method -> ReflectionUtils.invokeMethod(method, obj))
.orElseThrow(() -> newIllegalArgumentException("Method [%1$s] on Object of type [%2$s] not found",
methodName, org.springframework.util.ObjectUtils.nullSafeClassName(obj)));
}
/**
* Makes the {@link Constructor} accessible.
*
* @param constructor {@link Constructor} to make accessible; must not be {@literal null}.
* @return the given {@link Constructor}.
* @see java.lang.reflect.Constructor
*/
public static Constructor<?> makeAccessible(@NonNull Constructor<?> constructor) {
ReflectionUtils.makeAccessible(constructor);
return constructor;
}
/**
* Makes the {@link Field} accessible.
*
* @param field {@link Field} to make accessible; must not be {@literal null}.
* @return the given {@link Field}.
* @see java.lang.reflect.Field
*/
public static Field makeAccessible(Field field) {
ReflectionUtils.makeAccessible(field);
return field;
}
/**
* Makes the {@link Method} accessible.
*
* @param method {@link Method} to make accessible; must not be {@literal null}.
* @return the given {@link Method}.
* @see java.lang.reflect.Method
*/
public static Method makeAccessible(Method method) {
ReflectionUtils.makeAccessible(method);
return method;
}
/**
* Returns the given {@link Object value} or throws an {@link IllegalArgumentException}
* if {@link Object value} is {@literal null}.
*
* @param <T> {@link Class type} of the {@link Object value}.
* @param value {@link Object} to return.
* @return the {@link Object value} or throw an {@link IllegalArgumentException}
* if {@link Object value} is {@literal null}.
* @see #returnValueThrowOnNull(Object, RuntimeException)
*/
public static <T> T returnValueThrowOnNull(T value) {
return returnValueThrowOnNull(value, newIllegalArgumentException("Value must not be null"));
}
/**
* Returns the given {@link Object value} or throws the given {@link RuntimeException}
* if {@link Object value} is {@literal null}.
*
* @param <T> {@link Class type} of the {@link Object value}.
* @param value {@link Object} to return.
* @param exception {@link RuntimeException} to throw if {@link Object value} is {@literal null}.
* @return the {@link Object value} or throw the given {@link RuntimeException}
* if {@link Object value} is {@literal null}.
*/
public static <T> T returnValueThrowOnNull(T value, RuntimeException exception) {
if (value == null) {
throw exception;
}
return value;
}
/**
* Resolves the {@link Object invocation target} for the given {@link Method}.
*
* If the {@link Method} is {@link Modifier#STATIC} then {@literal null} is returned,
* otherwise {@link Object target} will be returned.
*
* @param <T> {@link Class type} of the {@link Object target}.
* @param target {@link Object} on which the {@link Method} will be invoked.
* @param method {@link Method} to invoke on the {@link Object}.
* @return the resolved {@link Object invocation method}.
* @see java.lang.Object
* @see java.lang.reflect.Method
*/
public static <T> T resolveInvocationTarget(T target, Method method) {
return Modifier.isStatic(method.getModifiers()) ? null : target;
}
@FunctionalInterface
public interface ExceptionThrowingOperation<T> {
T run() throws Exception;
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2017-present 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.geode.core.util;
import java.util.Optional;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.boot.logging.LoggingSystemFactory;
import org.springframework.lang.NonNull;
import org.springframework.util.ClassUtils;
/**
* Abstract base class used to perform actions on Spring Boot configuration and components.
*
* @author John Blum
* @see org.springframework.geode.core.util.SpringExtensions
* @since 2.0.0
*/
@SuppressWarnings("unused")
public abstract class SpringBootExtensions extends SpringExtensions {
/**
* Cleans up all resources allocated by the {@link LoggingSystem} loaded, configured and initialized by Spring Boot.
*
* @see #cleanUpLoggingSystem(ClassLoader)
* @see java.lang.ClassLoader
*/
public static void cleanUpLoggingSystem() {
cleanUpLoggingSystem(ClassUtils.getDefaultClassLoader());
}
/**
* Cleans up all resources allocated by the {@link LoggingSystem} loaded, configured and initialized by Spring Boot.
*
* @param classLoader Java {@link ClassLoader} used to resolve the Spring Boot {@link LoggingSystem}
* representing the logging provider (e.g. Logback).
* @see java.lang.ClassLoader
*/
public static void cleanUpLoggingSystem(@NonNull ClassLoader classLoader) {
Optional.ofNullable(LoggingSystemFactory.fromSpringFactories())
.map(loggingSystemFactory -> loggingSystemFactory.getLoggingSystem(classLoader))
.ifPresent(LoggingSystem::cleanUp);
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2017-present 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.geode.core.util;
import java.util.Optional;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* {@link SpringExtensions} is an abstract utility class containing functions to extend the functionality of Spring.
*
* @author John Blum
* @see org.springframework.beans.factory.BeanFactory
* @see org.springframework.beans.factory.config.BeanDefinition
* @see org.springframework.beans.factory.support.BeanDefinitionRegistry
* @see org.springframework.context.ApplicationContext
* @since 1.6.0
*/
@SuppressWarnings("unused")
public abstract class SpringExtensions {
public static final String BEAN_DEFINITION_METADATA_JSON = "{\n"
+ "\t'beanName': '%1$s',%n"
+ "\t'beanClassName': '%2$s',%n"
+ "\t'description': '%3$s',%n"
+ "\t'originatingBeanDefinition': '%4$s',%n"
+ "\t'parentName': '%5$s',%n"
+ "\t'resourceDescription': '%6$s',%n"
+ "\t'source': '%7$s',%n"
+ "}";
public static final String EMPTY_JSON_OBJECT = "{}";
public static @NonNull String getBeanDefinitionMetadata(@NonNull String beanName,
@Nullable ApplicationContext applicationContext) {
return Optional.ofNullable(applicationContext)
.filter(ConfigurableApplicationContext.class::isInstance)
.map(ConfigurableApplicationContext.class::cast)
.map(ConfigurableApplicationContext::getBeanFactory)
.map(beanFactory -> getBeanDefinitionMetadata(beanName, beanFactory))
.orElse(EMPTY_JSON_OBJECT);
}
public static @NonNull String getBeanDefinitionMetadata(@NonNull String beanName,
@Nullable BeanFactory beanFactory) {
return Optional.ofNullable(beanFactory)
.filter(BeanDefinitionRegistry.class::isInstance)
.map(BeanDefinitionRegistry.class::cast)
.map(registry -> getBeanDefinitionMetadata(beanName, registry))
.orElse(EMPTY_JSON_OBJECT);
}
public static @NonNull String getBeanDefinitionMetadata(@NonNull String beanName,
@Nullable BeanDefinitionRegistry beanDefinitionRegistry) {
return Optional.ofNullable(beanDefinitionRegistry)
.filter(registry -> StringUtils.hasText(beanName))
.map(registry -> registry.getBeanDefinition(beanName))
.map(beanDefinition -> getBeanDefinitionMetadata(beanName, beanDefinition))
.orElse(EMPTY_JSON_OBJECT);
}
public static @NonNull String getBeanDefinitionMetadata(@Nullable String beanName,
@Nullable BeanDefinition beanDefinition) {
if (beanDefinition != null) {
return String.format(BEAN_DEFINITION_METADATA_JSON, beanName,
beanDefinition.getBeanClassName(),
beanDefinition.getDescription(),
beanDefinition.getOriginatingBeanDefinition(),
beanDefinition.getParentName(),
beanDefinition.getResourceDescription(),
beanDefinition.getSource());
}
return EMPTY_JSON_OBJECT;
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2017-present 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.geode.core.util.function;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* Utility methods for using Java {@link Function Functions}.
*
* @author John Blum
* @see Function
* @since 1.1.0
*/
@SuppressWarnings("unused")
public abstract class FunctionUtils {
public static <T, R> Function<T, R> toNullReturningFunction(Consumer<T> consumer) {
return object -> {
consumer.accept(object);
return null;
};
}
}

View File

@@ -0,0 +1,390 @@
/*
* Copyright 2017-present 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.geode.data;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.geode.cache.Region;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.data.gemfire.util.ArrayUtils;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract base class implementing the {@link CacheDataImporter} and {@link CacheDataExporter} interfaces in order to
* simplify import/export data access operation implementations in a consistent way.
*
* @author John Blum
* @see java.util.function.Predicate
* @see org.apache.geode.cache.Region
* @see org.springframework.context.ApplicationContext
* @see org.springframework.context.ApplicationContextAware
* @see org.springframework.context.EnvironmentAware
* @see org.springframework.core.env.Environment
* @see org.springframework.geode.data.CacheDataImporterExporter
* @since 1.3.0
*/
@SuppressWarnings({ "rawtypes", "unused" })
public abstract class AbstractCacheDataImporterExporter
implements ApplicationContextAware, CacheDataImporterExporter, EnvironmentAware {
protected static final boolean DEFAULT_CACHE_DATA_EXPORT_ENABLED = false;
protected static final boolean DEFAULT_CACHE_DATA_IMPORT_ENABLED = true;
protected static final String CACHE_DATA_EXPORT_ENABLED_PROPERTY_NAME =
"spring.boot.data.gemfire.cache.data.export.enabled";
protected static final String CACHE_DATA_IMPORT_ACTIVE_PROFILES_PROPERTY_NAME =
"spring.boot.data.gemfire.cache.data.import.active-profiles";
protected static final String CACHE_DATA_IMPORT_ENABLED_PROPERTY_NAME =
"spring.boot.data.gemfire.cache.data.import.enabled";
protected static final String DEFAULT_CACHE_DATA_IMPORT_ACTIVE_PROFILES = "";
private static final String RESERVED_DEFAULT_PROFILE_NAME = "default";
private ApplicationContext applicationContext;
private Environment environment;
private final Logger logger = LoggerFactory.getLogger(getClass());
private Predicate<Region<?, ?>> regionPredicate = newRegionPredicate();
/**
* Constructs a new instance of {@link Predicate} used to filter {@link Region Regions} on data import/export.
*
* The default {@link Predicate} accepts all {@link Region Regions}. Override the {@link #getRegionPredicate()}
* method to change the default behavior.
*
* @return a new instance of {@link Predicate} used to filter {@link Region Regions} on data import/export.
* @see org.apache.geode.cache.Region
* @see java.util.function.Predicate
* @see #getRegionPredicate()
*/
private Predicate<Region<?, ?>> newRegionPredicate() {
return region -> true;
}
/**
* Sets a reference to a {@link ApplicationContext} used by this data importer/exporter to perform its function.
*
* @param applicationContext {@link ApplicationContext} used by this data importer/exporter.
* @see org.springframework.context.ApplicationContext
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/**
* Return an {@link Optional} reference to the configured {@link ApplicationContext} used by
* this data importer/exporter to perform its function.
*
* @return an {@link Optional} reference to the configured {@link ApplicationContext} used by
* this data importer/exporter.
* @see org.springframework.context.ApplicationContext
* @see java.util.Optional
*/
protected Optional<ApplicationContext> getApplicationContext() {
return Optional.ofNullable(this.applicationContext);
}
/**
* Returns a required reference to the configured {@link ApplicationContext} used by this data importer/exporter.
*
* @return a required reference to the configured {@link ApplicationContext} used by this data importer/exporter.
* @throws IllegalStateException if an {@link ApplicationContext} was not configured
* ({@link #setApplicationContext(ApplicationContext)} set).
* @see org.springframework.context.ApplicationContext
* @see #getApplicationContext()
*/
protected ApplicationContext requireApplicationContext() {
return getApplicationContext()
.orElseThrow(() -> newIllegalStateException("ApplicationContext was not configured"));
}
/**
* Sets a reference to the configured {@link Environment} used by this data importer/exporter
* to perform its function.
*
* @param environment reference to the configured {@link Environment}.
* @see org.springframework.core.env.Environment
*/
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
/**
* Returns an {@link Optional} reference to the configured {@link Environment} used by this data importer/exporter
* to access {@link Environment} specific configuration.
*
* @return an {@link Optional} reference to the configured {@link Environment} used by this data importer/exporter
* to access {@link Environment} specific configuration.
* @see org.springframework.core.env.Environment
* @see java.util.Optional
*/
protected Optional<Environment> getEnvironment() {
return Optional.ofNullable(this.environment);
}
/**
* Returns a required reference to the configured {@link Environment} used by this data importer/exporter
* to access {@link Environment} specific configuration.
*
* @return a required reference to the configured {@link Environment}.
* @throws IllegalStateException if the {@link Environment} was not configured
* ({@link #setEnvironment(Environment) set}).
* @see org.springframework.core.env.Environment
* @see #getEnvironment()
*/
protected Environment requireEnvironment() {
return getEnvironment()
.orElseThrow(() -> newIllegalStateException("Environment was not configured"));
}
/**
* Return the configured {@link Logger} to log messages.
*
* @return the configured {@link Logger}.
* @see org.slf4j.Logger
*/
protected Logger getLogger() {
return this.logger;
}
/**
* Returns the configured {@link Predicate} used to filter {@link Region Regions} on data import/export.
*
* @return the configured {@link Predicate} used to filter {@link Region Regions} on data import/export.
* @see org.apache.geode.cache.Region
* @see java.util.function.Predicate
*/
protected @NonNull Predicate<Region<?, ?>> getRegionPredicate() {
return Optional.ofNullable(this.regionPredicate)
.orElseGet(this::newRegionPredicate);
}
/**
* Null-safe method to determine whether export has been explicitly configured and enabled or disabled.
*
* @param environment {@link Environment} used to assess the configuration of export.
* @return a boolean value indicating whether the export is enabled ({@literal true})
* or disabled ({@literal false}); {@literal false} by default.
* @see org.springframework.core.env.Environment
*/
protected boolean isExportEnabled(@Nullable Environment environment) {
return environment != null
&& Boolean.TRUE.equals(environment.getProperty(CACHE_DATA_EXPORT_ENABLED_PROPERTY_NAME, Boolean.class,
DEFAULT_CACHE_DATA_EXPORT_ENABLED));
}
/**
* Exports data contained in the given {@link Region}.
*
* @param region {@link Region} to export data from.
* @return the given {@link Region}.
* @see org.apache.geode.cache.Region
* @see #isExportEnabled(Environment)
* @see #getRegionPredicate()
* @see #doExportFrom(Region)
*/
@NonNull @Override
public Region exportFrom(@NonNull Region region) {
Assert.notNull(region, "Region must not be null");
boolean exportEnabled = getEnvironment()
.filter(this::isExportEnabled)
.filter(environment -> getRegionPredicate().test(region))
.isPresent();
return exportEnabled ? doExportFrom(region) : region;
}
/**
* Exports data contained in the given {@link Region}.
*
* @param region {@link Region} to export data from.
* @return the given {@link Region}.
* @see org.apache.geode.cache.Region
* @see #exportFrom(Region)
*/
protected abstract @NonNull Region doExportFrom(@NonNull Region region);
/**
* Null-safe method to determine whether import has been explicitly configured and enabled or disabled.
*
* @param environment {@link Environment} used to assess the configuration of the import.
* @return a boolean value indicating whether the import is enabled ({@literal true})
* or disabled ({@literal false}).
* @see org.springframework.core.env.Environment
*/
protected boolean isImportEnabled(@Nullable Environment environment) {
return environment != null
&& Boolean.TRUE.equals(environment.getProperty(CACHE_DATA_IMPORT_ENABLED_PROPERTY_NAME, Boolean.class,
DEFAULT_CACHE_DATA_IMPORT_ENABLED));
}
/**
* Determines whether the Cache Data Import data access operation is enabled based on the configured, active/default
* {@literal Profiles} as declared in the Spring {@link Environment}.
*
* @param environment {@link Environment} used to evaluate the configured, active {@literal Profiles};
* must not be {@literal null}.
* @return a boolean value indicating whether the the Cache Data Import data access operation is enabled based on
* the configured, active/default {@literal Profiles}.
* @throws IllegalArgumentException if {@link Environment} is {@literal null}.
* @see org.springframework.core.env.Environment
* @see #useDefaultProfilesIfEmpty(Environment, Set)
* @see #getActiveProfiles(Environment)
*/
protected boolean isImportProfilesActive(@NonNull Environment environment) {
Assert.notNull(environment, "Environment must not be null");
boolean importProfilesActive = true;
String cacheDataImportActiveProfiles =
environment.getProperty(CACHE_DATA_IMPORT_ACTIVE_PROFILES_PROPERTY_NAME,
DEFAULT_CACHE_DATA_IMPORT_ACTIVE_PROFILES);
Set<String> cacheDataImportProfiles = commaDelimitedStringToSet(cacheDataImportActiveProfiles);
if (!cacheDataImportProfiles.isEmpty()) {
Set<String> configuredProfiles = useDefaultProfilesIfEmpty(environment, getActiveProfiles(environment));
// The configured, "Active Profiles" must contain at least 1 of the configured cacheDataImportProfiles.
importProfilesActive = CollectionUtils.containsAny(configuredProfiles, cacheDataImportProfiles);
}
return importProfilesActive;
}
/**
* Imports data into the given {@link Region}.
*
* @param region {@link Region} to import data into.
* @return the given {@link Region}.
* @see org.apache.geode.cache.Region
* @see #isImportEnabled(Environment)
* @see #isImportProfilesActive(Environment)
* @see #getRegionPredicate()
* @see #doImportInto(Region)
*/
@NonNull @Override
public Region importInto(@NonNull Region region) {
Assert.notNull(region, "Region must not be null");
boolean importEnabled = getEnvironment()
.filter(this::isImportEnabled)
.filter(this::isImportProfilesActive)
.filter(environment -> getRegionPredicate().test(region))
.isPresent();
return importEnabled ? doImportInto(region) : region;
}
/**
* Imports data into the given {@link Region}.
*
* @param region {@link Region} to import data into.
* @return the given {@link Region}.
* @see org.apache.geode.cache.Region
* @see #importInto(Region)
*/
protected abstract @NonNull Region doImportInto(@NonNull Region region);
@NonNull Set<String> commaDelimitedStringToSet(@Nullable String commaDelimitedString) {
return StringUtils.hasText(commaDelimitedString)
? Arrays.stream(commaDelimitedString.split(","))
.map(String::trim)
.filter(StringUtils::hasText)
.collect(Collectors.toSet())
: Collections.emptySet();
}
@NonNull Set<String> getActiveProfiles(@NonNull Environment environment) {
return environment != null
? toSet(environment.getActiveProfiles(), String.class)
: Collections.emptySet();
}
@NonNull Set<String> useDefaultProfilesIfEmpty(@NonNull Environment environment,
@Nullable Set<String> activeProfiles) {
Set<String> resolvedProfiles = CollectionUtils.nullSafeSet(activeProfiles).stream()
.filter(StringUtils::hasText)
.collect(Collectors.toSet());
if (resolvedProfiles.isEmpty()) {
Set<String> defaultProfiles = environment != null
? toSet(environment.getDefaultProfiles(), String.class).stream()
.filter(StringUtils::hasText)
.collect(Collectors.toSet())
: Collections.emptySet();
if (isNotDefaultProfileOnlySet(defaultProfiles)) {
resolvedProfiles = defaultProfiles;
}
}
return resolvedProfiles;
}
// The Set of configured Profiles cannot be null, empty or contain only the "default" Profile.
boolean isNotDefaultProfileOnlySet(@Nullable Set<String> profiles) {
return Objects.nonNull(profiles)
&& !profiles.isEmpty()
&& !Collections.singleton(RESERVED_DEFAULT_PROFILE_NAME).containsAll(profiles);
}
private static @NonNull <T> Set<T> toSet(@Nullable T[] array, @NonNull Class<T> type) {
return CollectionUtils.asSet(ArrayUtils.nullSafeArray(array, type));
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2017-present 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.geode.data;
import org.apache.geode.cache.Region;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
import org.springframework.data.gemfire.ResolvableRegionFactoryBean;
import org.springframework.lang.NonNull;
/**
* The {@link CacheDataExporter} interface is a {@link FunctionalInterface} defining a contract for exporting data
* from a cache {@link Region}.
*
* @author John Blum
* @see java.lang.FunctionalInterface
* @see org.apache.geode.cache.Region
* @see org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor
* @see org.springframework.data.gemfire.ResolvableRegionFactoryBean
* @since 1.3.0
*/
@FunctionalInterface
@SuppressWarnings("rawtypes")
public interface CacheDataExporter extends DestructionAwareBeanPostProcessor {
/**
* Exports any data contained in a {@link Region} on destruction.
*
* @param bean {@link Object} bean to evaluate.
* @param beanName {@link String} containing the name of the bean.
* @throws BeansException if exporting data from a {@link Region} fails!
* @see org.apache.geode.cache.Region
* @see #exportFrom(Region)
*/
@Override
default void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {
if (bean instanceof Region) {
exportFrom((Region) bean);
}
else if (bean instanceof ResolvableRegionFactoryBean) {
exportFrom(((ResolvableRegionFactoryBean) bean).getRegion());
}
}
/**
* Exports data contained in the given {@link Region}.
*
* @param region {@link Region} to export data from.
* @return the given {@link Region}.
* @see org.apache.geode.cache.Region
*/
@NonNull Region exportFrom(@NonNull Region region);
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2017-present 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.geode.data;
import org.apache.geode.cache.Region;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* The {@link CacheDataImporter} interface is a {@link FunctionalInterface} defininig a contract for importing data
* into a cache {@link Region}.
*
* @author John Blum
* @see java.lang.FunctionalInterface
* @see org.apache.geode.cache.Region
* @see org.springframework.beans.factory.config.BeanPostProcessor
* @since 1.3.0
*/
@FunctionalInterface
@SuppressWarnings("rawtypes")
public interface CacheDataImporter extends BeanPostProcessor {
/**
* Imports data from an external data source into a given {@link Region} after initialization.
*
* @param bean {@link Object} bean to evaluate.
* @param beanName {@link String} containing the name of the bean.
* @throws BeansException if importing data into a {@link Region} fails!
* @see org.apache.geode.cache.Region
* @see #importInto(Region)
*/
@Nullable @Override
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof Region) {
bean = importInto((Region) bean);
}
return bean;
}
/**
* Imports data into the given {@link Region}.
*
* @param region {@link Region} to import data into.
* @return the given {@link Region}.
* @see org.apache.geode.cache.Region
*/
@NonNull Region importInto(@NonNull Region region);
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2017-present 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.geode.data;
import org.apache.geode.cache.GemFireCache;
/**
* Convenient {@link Class#isInterface() interface} to extend when the implementation supports both
* data import and export from/to a {@link GemFireCache}.
*
* @author John Blum
* @see org.apache.geode.cache.GemFireCache
* @see org.springframework.geode.data.CacheDataExporter
* @see org.springframework.geode.data.CacheDataImporter
* @since 1.3.0
*/
public interface CacheDataImporterExporter extends CacheDataExporter, CacheDataImporter {
}

View File

@@ -0,0 +1,259 @@
/*
* Copyright 2017-present 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.geode.data.json;
import java.util.Arrays;
import org.apache.geode.cache.Region;
import org.apache.geode.pdx.PdxInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.data.gemfire.util.ArrayUtils;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.geode.data.CacheDataExporter;
import org.springframework.geode.data.CacheDataImporter;
import org.springframework.geode.data.json.converter.AbstractObjectArrayToJsonConverter;
import org.springframework.geode.data.json.converter.JsonToPdxArrayConverter;
import org.springframework.geode.data.json.converter.support.JacksonJsonToPdxConverter;
import org.springframework.geode.data.support.ResourceCapableCacheDataImporterExporter;
import org.springframework.geode.pdx.ObjectPdxInstanceAdapter;
import org.springframework.geode.pdx.PdxInstanceWrapper;
import org.springframework.geode.util.CacheUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
/**
* The {@link JsonCacheDataImporterExporter} class is a {@link CacheDataImporter} and {@link CacheDataExporter}
* implementation that can export/import JSON data to/from a {@link Resource} given a target {@link Region}.
*
* @author John Blum
* @see org.apache.geode.cache.Region
* @see org.apache.geode.pdx.PdxInstance
* @see org.springframework.core.io.Resource
* @see org.springframework.geode.data.CacheDataExporter
* @see org.springframework.geode.data.CacheDataImporter
* @see org.springframework.geode.data.json.converter.JsonToPdxArrayConverter
* @see org.springframework.geode.data.json.converter.ObjectArrayToJsonConverter
* @see org.springframework.geode.data.json.converter.support.JacksonJsonToPdxConverter
* @see org.springframework.geode.data.support.ResourceCapableCacheDataImporterExporter
* @see org.springframework.geode.pdx.ObjectPdxInstanceAdapter
* @see org.springframework.geode.pdx.PdxInstanceWrapper
* @see org.springframework.stereotype.Component
* @since 1.3.0
*/
@Component
@SuppressWarnings("rawtypes")
public class JsonCacheDataImporterExporter extends ResourceCapableCacheDataImporterExporter {
protected static final PdxInstance[] EMPTY_PDX_INSTANCE_ARRAY = {};
@Autowired(required = false)
private JsonToPdxArrayConverter jsonToPdxArrayConverter;
private final RegionValuesToJsonConverter regionValuesToJsonConverter = new RegionValuesToJsonConverter();
/**
* Determines whether the given array is empty or not. An array is not empty if the array reference
* is not {@literal null} and contains at least 1 element.
*
* @param <T> {@link Class type} of the array elements.
* @param array {@link Object} array to evaluate.
* @return a boolean value indicating whether the array is empty or not.
*/
@SuppressWarnings("unchecked")
private static <T> boolean isNotEmpty(T... array) {
return array != null && array.length > 0;
}
/**
* Initializes the JSON to PDX (array) converter.
*
* @see #newJsonToPdxArrayConverter()
*/
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
this.jsonToPdxArrayConverter = this.jsonToPdxArrayConverter != null
? this.jsonToPdxArrayConverter
: newJsonToPdxArrayConverter();
}
private @NonNull JsonToPdxArrayConverter newJsonToPdxArrayConverter() {
return new JacksonJsonToPdxConverter();
}
/**
* Returns a reference to the configured {@link JsonToPdxArrayConverter}.
*
* @return a reference to the configured {@link JsonToPdxArrayConverter}.
* @see org.springframework.geode.data.json.converter.JsonToPdxArrayConverter
*/
protected @NonNull JsonToPdxArrayConverter getJsonToPdxArrayConverter() {
return this.jsonToPdxArrayConverter;
}
/**
* @inheritDoc
*/
@NonNull @Override
public Region doExportFrom(@NonNull Region region) {
Assert.notNull(region, "Region must not be null");
getExportResourceResolver()
.resolve(region)
.ifPresent(resource -> {
String json = toJson(region);
getLogger().debug("Saving JSON [{}] from Region [{}]", json, region.getName());
getResourceWriter().write(resource, json.getBytes());
});
return region;
}
/**
* @inheritDoc
*/
@NonNull @Override
public Region doImportInto(@NonNull Region region) {
Assert.notNull(region, "Region must not be null");
getImportResourceResolver()
.resolve(region)
.map(this.getResourceReader()::read)
.map(this::toPdx)
.ifPresent(pdxInstances -> regionPutPdx(region, pdxInstances));
return region;
}
/**
* Puts all PDX data from the {@link PdxInstance} array into the target {@link Region} mapped to
* the PDX {@link PdxInstance#isIdentityField(String) identifier} as the {@literal key}.
*
* @param region target {@link Region} to store the PDX data; must not be {@literal null}
* @param pdx {@link PdxInstance} array containing the PDX data to store in the target {@link Region}.
* @see org.apache.geode.cache.Region
* @see org.apache.geode.cache.Region#put(Object, Object)
* @see org.apache.geode.pdx.PdxInstance
*/
@SuppressWarnings("unchecked")
void regionPutPdx(@NonNull Region region, @Nullable PdxInstance[] pdx) {
Arrays.stream(ArrayUtils.nullSafeArray(pdx, PdxInstance.class)).forEach(pdxInstance ->
region.put(resolveKey(pdxInstance), resolveValue(pdxInstance)));
}
/**
* Post processes the given {{@link PdxInstance}.
*
* @param pdxInstance {@link PdxInstance} to process.
* @return the {@link PdxInstance}.
* @see org.apache.geode.pdx.PdxInstance
*/
protected PdxInstance postProcess(PdxInstance pdxInstance) {
return pdxInstance;
}
/**
* Resolves the {@link Object key} used to map the given {@link PdxInstance} as the {@link Object value}
* for the {@code Region.Entry} stored in the {@link Region}.
*
* @param pdxInstance {@link PdxInstance} used to resolve the {@link Object key}.
* @return the resolved {@link Object key}.
* @see org.springframework.geode.pdx.PdxInstanceWrapper#getIdentifier()
* @see org.apache.geode.pdx.PdxInstance
*/
protected @NonNull Object resolveKey(@NonNull PdxInstance pdxInstance) {
return PdxInstanceWrapper.from(pdxInstance).getIdentifier();
}
/**
* Resolves the {@link Object value} to store in the {@link Region} from the given {@link PdxInstance}.
*
* If the given {@link PdxInstance} is an instance of {@link PdxInstanceWrapper} then this method will return
* the underlying, {@link PdxInstanceWrapper#getDelegate() delegate} {@link PdxInstance}.
*
* If the given {@link PdxInstance} is an instance of {@link ObjectPdxInstanceAdapter} then this method will return
* the underlying, {@link ObjectPdxInstanceAdapter#getObject() Object}.
*
* Otherwise, the given {@link PdxInstance} is returned.
*
* @param pdxInstance {@link PdxInstance} to unwrap.
* @return the resolved {@link Object value}.
* @see org.springframework.geode.pdx.ObjectPdxInstanceAdapter#unwrap(PdxInstance)
* @see org.springframework.geode.pdx.PdxInstanceWrapper#unwrap(PdxInstance)
* @see org.apache.geode.pdx.PdxInstance
* @see #postProcess(PdxInstance)
*/
protected @Nullable Object resolveValue(@Nullable PdxInstance pdxInstance) {
return ObjectPdxInstanceAdapter.unwrap(PdxInstanceWrapper.unwrap(postProcess(pdxInstance)));
}
/**
* Convert {@link Object values} contained in the {@link Region} to {@link String JSON}.
*
* @param region {@link Region} to process; must not be {@literal null}.
* @return {@link String JSON} containing the {@link Object values} from the given {@link Region}.
* @see org.apache.geode.cache.Region
*/
@SuppressWarnings("unchecked")
protected @NonNull String toJson(@NonNull Region region) {
return this.regionValuesToJsonConverter.convert(region);
}
/**
* Converts the array of {@link Byte#TYPE bytes} containing multiple {@link String JSON} objects
* into an array of {@link PdxInstance PdxInstances}.
*
* @param json array of {@link Byte#TYPE bytes} containing the {@link String JSON} to convert to PDX.
* @return an array of {@link PdxInstance PdxInstances} for each {@link String JSON} object.
* @see org.apache.geode.pdx.PdxInstance
* @see #getJsonToPdxArrayConverter()
*/
protected @NonNull PdxInstance[] toPdx(@NonNull byte[] json) {
return isNotEmpty(json)
? getJsonToPdxArrayConverter().convert(json)
: EMPTY_PDX_INSTANCE_ARRAY;
}
/**
* Converts all {@link Region#values() values} in the targeted {@link Region} into {@literal JSON}.
*
* The converter is capable of handling both {@link Object Objects} and PDX.
*
* @see org.springframework.geode.data.json.converter.AbstractObjectArrayToJsonConverter
*/
static class RegionValuesToJsonConverter extends AbstractObjectArrayToJsonConverter {
@NonNull <K, V> String convert(@NonNull Region<K, V> region) {
Assert.notNull(region, "Region must not be null");
return super.convert(CollectionUtils.nullSafeCollection(CacheUtils.collectValues(region)));
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2017-present 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.geode.data.json.converter;
import java.util.Map;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.geode.data.json.converter.support.JSONFormatterPdxToJsonConverter;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* An abstract base class implementing {@link ObjectArrayToJsonConverter} encapsulating functionality common
* to all implementations.
*
* @author John Blum
* @see java.lang.Iterable
* @see java.util.Map
* @see org.springframework.geode.data.json.converter.ObjectArrayToJsonConverter
* @since 1.3.0
*/
public abstract class AbstractObjectArrayToJsonConverter implements ObjectArrayToJsonConverter {
protected static final String BEGIN_ARRAY = "[";
protected static final String EMPTY_STRING = "";
protected static final String END_ARRAY = "]";
protected static final String JSON_OBJECT_SEPARATOR = ", ";
private ObjectToJsonConverter converter = newObjectToJsonConverter();
// TODO configure via an SPI
private @NonNull ObjectToJsonConverter newObjectToJsonConverter() {
return new JSONFormatterPdxToJsonConverter();
}
/**
* Returns a reference to the configured {@link ObjectToJsonConverter} used to convert
* individual {@link Object Objects} into {@link String JSON}.
*
* @return a reference to the configured {@link ObjectToJsonConverter}; never {@literal null}.
* @see org.springframework.geode.data.json.converter.ObjectToJsonConverter
*/
protected @NonNull ObjectToJsonConverter getObjectToJsonConverter() {
return this.converter;
}
/**
* Converts the given {@link Iterable} of {@link Object Objects} into a {@link String JSON} array.
*
* @param iterable {@link Iterable} containing the {@link Object Objects} to convert into {@link String JSON};
* must not be {@literal null}.
* @return the {@link String JSON} generated from the given {@link Iterable} of {@link Object Objects};
* never {@literal null}.
* @throws IllegalArgumentException if {@link Iterable} is {@literal null}.
* @see #getObjectToJsonConverter()
* @see java.lang.Iterable
*/
@Override
public @NonNull String convert(@NonNull Iterable<?> iterable) {
Assert.notNull(iterable, "Iterable must not be null");
StringBuilder json = new StringBuilder(BEGIN_ARRAY);
ObjectToJsonConverter converter = getObjectToJsonConverter();
boolean addComma = false;
for (Object value : CollectionUtils.nullSafeIterable(iterable)) {
json.append(addComma ? JSON_OBJECT_SEPARATOR : EMPTY_STRING);
json.append(converter.convert(value));
addComma = true;
}
json.append(END_ARRAY);
return json.toString();
}
/**
* Converts the {@link Map#values() values} from the given {@link Map} into {@link String JSON}.
*
* @param <K> {@link Class} type of the {@link Map#keySet() keys}.
* @param <V> {@link Class} type of the {@link Map#values() values}.
* @param map {@link Map} containing the {@link Map#values() values} to convert into {@link String JSON}.
* @return {@link String JSON} generated from the {@link Map#values() values} in the given {@link Map}.
* @see #convert(Iterable)
* @see java.util.Map
*/
public @NonNull <K, V> String convert(@Nullable Map<K, V> map) {
return convert(CollectionUtils.nullSafeMap(map).values());
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2017-present 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.geode.data.json.converter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
/**
* Spring {@link Converter} interface extension defining a contract to convert {@link String JSON}
* to an array of {@link Object Objects}.
*
* @author John Blum
* @see java.lang.Object
* @see java.lang.String
* @see org.springframework.core.convert.converter.Converter
* @since 1.3.0
*/
public interface JsonToObjectArrayConverter extends Converter<String, Object[]> {
/**
* Converts the array of {@link Byte#TYPE bytes} containing JSON into an array of {@link Object Objects}.
*
* @param json array of {@link Byte#TYPE bytes} containing the JSON to convert; must not be {@literal null}.
* @return an array of {@link Object Objects} converted from the array of {@link Byte#TYPE bytes} containing JSON.
* @see #convert(Object)
*/
default @NonNull Object[] convert(@NonNull byte[] json) {
return convert(new String(json));
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2017-present 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.geode.data.json.converter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
/**
* Spring {@link Converter} interface extension defining a contract to convert
* from {@link String JSON} to an {@link Object} (POJO).
*
* @author John Blum
* @see java.lang.Object
* @see java.lang.String
* @see org.springframework.core.convert.converter.Converter
* @since 1.3.0
*/
public interface JsonToObjectConverter extends Converter<String, Object> {
/**
* Converts the array of {@link Byte#TYPE bytes} containing JSON into an {@link Object} (POJO).
*
* @param json array of {@link Byte#TYPE bytes} containing JSON to convert into an {@link Object} (POJO);
* must not be {@literal null}.
* @return an {@link Object} (POJO) converted from the array of {@link Byte#TYPE bytes} containing JSON.
* @see #convert(Object)
*/
default @NonNull Object convert(@NonNull byte[] json) {
return convert(new String(json));
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2017-present 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.geode.data.json.converter;
import org.apache.geode.pdx.PdxInstance;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
/**
* A Spring {@link Converter} interface extension defining a contract to convert
* from {@link String JSON} to an array of {@link PdxInstance} objects.
*
* @author John Blum
* @see java.lang.FunctionalInterface
* @see java.lang.String
* @see org.apache.geode.pdx.PdxInstance
* @see org.springframework.core.convert.converter.Converter
* @since 1.3.0
*/
@FunctionalInterface
public interface JsonToPdxArrayConverter extends Converter<String, PdxInstance[]> {
/**
* Converts the array of {@link Byte#TYPE bytes} containing JSON into an array of {@link PdxInstance} objects.
*
* @param json array of {@link Byte#TYPE bytes} containing the JSON to convert; must not be {@literal null}.
* @return an array of {@link PdxInstance} objects converted from the array of {@link Byte#TYPE bytes}
* containing JSON.
* @see org.apache.geode.pdx.PdxInstance
* @see #convert(Object)
*/
default @NonNull PdxInstance[] convert(@NonNull byte[] json) {
return convert(new String(json));
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2017-present 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.geode.data.json.converter;
import org.apache.geode.pdx.PdxInstance;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
/**
* A Spring {@link Converter} interface extension defining a contract to convert
* from {@link String JSON} to a {@link PdxInstance}.
*
* @author John Blum
* @see java.lang.FunctionalInterface
* @see java.lang.String
* @see org.apache.geode.pdx.PdxInstance
* @see org.springframework.core.convert.converter.Converter
* @since 1.3.0
*/
@FunctionalInterface
public interface JsonToPdxConverter extends Converter<String, PdxInstance> {
/**
* Converts the array of {@link Byte#TYPE bytes} containing JSON into a {@link PdxInstance}.
*
* @param json array of {@link Byte#TYPE bytes} containing JSON to convert into a {@link PdxInstance};
* must not be {@literal null}.
* @return a {@link PdxInstance} converted from the array of {@link Byte#TYPE bytes} containing JSON.
* @see #convert(Object)
*/
default @NonNull PdxInstance convert(@NonNull byte[] json) {
return convert(new String(json));
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2017-present 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.geode.data.json.converter;
import java.util.Arrays;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.gemfire.util.ArrayUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* A Spring {@link Converter} interface extension defining a contract to convert
* an {@link Iterable} or an array of {@link Object Objects} into {@link String JSON}.
*
* @author John Blum
* @see java.lang.FunctionalInterface
* @see java.lang.Iterable
* @see java.lang.Object
* @see java.lang.String
* @see org.springframework.core.convert.converter.Converter
* @since 1.3.0
*/
@FunctionalInterface
public interface ObjectArrayToJsonConverter extends Converter<Iterable<?>, String> {
/**
* Converts the given array of {@link Object Objects} into {@link String JSON}.
*
* @param array array of {@link Object Objects} to convert into {@link String JSON}.
* @return {@link String JSON} generated from the given array of {@link Object Objects}.
* @see #convert(Object)
*/
default @NonNull String convert(@Nullable Object... array) {
return convert(Arrays.asList(ArrayUtils.nullSafeArray(array, Object.class)));
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2017-present 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.geode.data.json.converter;
import org.springframework.core.convert.converter.Converter;
/**
* A Spring {@link Converter} interface extension defining a contract to convert
* from an {@link Object} into a {@link String JSON}.
*
* @author John Blum
* @see java.lang.FunctionalInterface
* @see java.lang.Object
* @see java.lang.String
* @see org.springframework.core.convert.converter.Converter
* @since 1.3.0
*/
@FunctionalInterface
public interface ObjectToJsonConverter extends Converter<Object, String> {
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright 2017-present 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.geode.data.json.converter.support;
import org.apache.geode.pdx.JSONFormatter;
import org.apache.geode.pdx.JSONFormatterException;
import org.apache.geode.pdx.PdxInstance;
import org.springframework.geode.data.json.converter.JsonToObjectConverter;
import org.springframework.geode.data.json.converter.JsonToPdxConverter;
import org.springframework.geode.pdx.ObjectPdxInstanceAdapter;
import org.springframework.geode.pdx.PdxInstanceWrapper;
import org.springframework.lang.NonNull;
/**
* A {@link JsonToPdxConverter} implementation using the Apache Geode {@link JSONFormatter} to convert
* from a {@literal JSON} {@link String} to a {@link PdxInstance}.
*
* @author John Blum
* @see org.apache.geode.pdx.JSONFormatter
* @see org.apache.geode.pdx.PdxInstance
* @see org.springframework.geode.data.json.converter.JsonToPdxConverter
* @since 1.3.0
*/
public class JSONFormatterJsonToPdxConverter implements JsonToPdxConverter {
private JsonToObjectConverter converter = newJsonToObjectConverter();
// TODO configure via an SPIs
private JsonToObjectConverter newJsonToObjectConverter() {
return new JacksonJsonToObjectConverter();
}
/**
* Returns a reference to the configured {@link JsonToObjectConverter} used to convert from {@link String JSON}
* to an {@link Object}.
*
* @return a reference to the configured {@link JsonToObjectConverter}; never {@literal null}.
* @see org.springframework.geode.data.json.converter.JsonToObjectConverter
*/
protected @NonNull JsonToObjectConverter getJsonToObjectConverter() {
return this.converter;
}
/**
* @inheritDoc
*/
@Override
public final @NonNull PdxInstance convert(@NonNull String json) {
try {
return convertJsonToPdx(json);
}
catch (JSONFormatterException cause) {
return convertJsonToObjectToPdx(json);
}
}
/**
* Adapts the given {@link Object} as a {@link PdxInstance}.
*
* @param target {@link Object} to adapt as PDX; must not be {@literal null}.
* @return a {@link PdxInstance} representing the given {@link Object}.
* @see org.springframework.geode.pdx.ObjectPdxInstanceAdapter#from(Object)
* @see org.apache.geode.pdx.PdxInstance
*/
protected @NonNull PdxInstance adapt(@NonNull Object target) {
return ObjectPdxInstanceAdapter.from(target);
}
/**
* Converts the given {@link String JSON} into a {@link Object} and then adapts the {@link Object}
* as a {@link PdxInstance}.
*
* @param json {@link String JSON} to convert into an {@link Object} into PDX.
* @return a {@link PdxInstance} converted from the given {@link String JSON}.
* @see org.apache.geode.pdx.PdxInstance
* @see #getJsonToObjectConverter()
* @see #adapt(Object)
*/
protected @NonNull PdxInstance convertJsonToObjectToPdx(@NonNull String json) {
return adapt(getJsonToObjectConverter().convert(json));
}
/**
* Converts the given {@link String JSON} to {@link PdxInstance PDX}.
*
* @param json {@link String} containing JSON to convert to PDX; must not be {@literal null}.
* @return JSON for the given {@link PdxInstance PDX}.
* @see org.apache.geode.pdx.PdxInstance
* @see #jsonFormatterFromJson(String)
* @see #wrap(PdxInstance)
*/
protected @NonNull PdxInstance convertJsonToPdx(@NonNull String json) {
return wrap(jsonFormatterFromJson(json));
}
/**
* Converts {@link String JSON} into {@link PdxInstance PDX} using {@link JSONFormatter#fromJSON(String)}.
*
* @param json {@link String JSON} to convert to {@link PdxInstance PDX}; must not be {@literal null}.
* @return {@link PdxInstance PDX} generated from the given, required {@link String JSON}; never {@literal null}.
* @see org.apache.geode.pdx.JSONFormatter#fromJSON(String)
* @see org.apache.geode.pdx.PdxInstance
*/
protected @NonNull PdxInstance jsonFormatterFromJson(@NonNull String json) {
return JSONFormatter.fromJSON(json);
}
/**
* Wraps the given {@link PdxInstance} in a new instance of {@link PdxInstanceWrapper}.
*
* @param pdxInstance {@link PdxInstance} to wrap.
* @return a new instance of {@link PdxInstanceWrapper} wrapping the given {@link PdxInstance}.
* @see org.springframework.geode.pdx.PdxInstanceWrapper#from(PdxInstance)
* @see org.springframework.geode.pdx.PdxInstanceWrapper
* @see org.apache.geode.pdx.PdxInstance
*/
protected @NonNull PdxInstanceWrapper wrap(@NonNull PdxInstance pdxInstance) {
return PdxInstanceWrapper.from(pdxInstance);
}
}

View File

@@ -0,0 +1,256 @@
/*
* Copyright 2017-present 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.geode.data.json.converter.support;
import java.util.Optional;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.geode.pdx.JSONFormatter;
import org.apache.geode.pdx.PdxInstance;
import org.apache.geode.pdx.WritablePdxInstance;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.geode.data.json.converter.ObjectToJsonConverter;
import org.springframework.geode.pdx.PdxInstanceBuilder;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* An {@link ObjectToJsonConverter} implementation using the Apache Geode {@link JSONFormatter} to convert
* from a {@link PdxInstance} to a {@literal JSON} {@link String}.
*
* @author John Blum
* @see com.fasterxml.jackson.databind.JsonNode
* @see com.fasterxml.jackson.databind.ObjectMapper
* @see com.fasterxml.jackson.databind.node.ObjectNode
* @see org.apache.geode.pdx.JSONFormatter
* @see org.apache.geode.pdx.PdxInstance
* @see org.apache.geode.pdx.WritablePdxInstance
* @see org.springframework.geode.data.json.converter.ObjectToJsonConverter
* @see org.springframework.geode.data.json.converter.support.JacksonObjectToJsonConverter
* @see org.springframework.geode.pdx.PdxInstanceBuilder
* @since 1.3.0
*/
public class JSONFormatterPdxToJsonConverter extends JacksonObjectToJsonConverter {
/**
* @inheritDoc
*/
@Nullable @Override
public final String convert(@Nullable Object source) {
return Optional.ofNullable(source)
.filter(PdxInstance.class::isInstance)
.map(PdxInstance.class::cast)
.map(this::convertPdxToJson)
.orElseGet(() -> convertPojoToJson(source));
}
/**
* Converts the given {@link Object} to JSON.
*
* @param source {@link Object} to convert to JSON.
* @return the JSON generated from the given {@link Object}.
* @see JacksonObjectToJsonConverter#convert(Object)
*/
protected @Nullable String convertPojoToJson(Object source) {
return super.convert(source);
}
/**
* Converts the given {@link PdxInstance PDX} to {@link String JSON}.
*
* @param pdxInstance {@link PdxInstance} to convert to JSON; must not be {@literal null}.
* @return JSON generated from the given {@link PdxInstance}.
* @see org.apache.geode.pdx.JSONFormatter#toJSON(PdxInstance)
* @see org.apache.geode.pdx.PdxInstance
* @see #jsonFormatterToJson(PdxInstance)
*/
protected @NonNull String convertPdxToJson(@NonNull PdxInstance pdxInstance) {
return decorate(pdxInstance, jsonFormatterToJson(pdxInstance));
}
/**
* Converts {@link PdxInstance PDX} into {@link String JSON} using {@link JSONFormatter#toJSON(PdxInstance)}.
*
* @param pdxInstance {@link PdxInstance PDX} to convert to {@link String JSON}; must not be {@literal null}.
* @return {@link String JSON} generated from the given, required {@link PdxInstance PDX}; never {@literal null}.
* @see org.apache.geode.pdx.JSONFormatter#toJSON(PdxInstance)
* @see org.apache.geode.pdx.PdxInstance
*/
@NonNull String jsonFormatterToJson(@NonNull PdxInstance pdxInstance) {
return JSONFormatter.toJSON(pdxInstance);
}
/**
* WARNING!!!
*
* First, this method might be less than optimal and could lead to PDX type explosion!
*
* Second, this {@code pdxInstance.createWriter().setField(AT_TYPE_METADATA_PROPERTY_NAME, className);} ...
*
* Throws:
* org.apache.geode.pdx.PdxFieldDoesNotExistException: A field named @type does not exist on ...
* PdxType[dsid=0,typenum=7232261,name=example.app.crm.model.Customer,fields=[id:long:identity:0:idx0(relativeOffset)=0:idx1(vlfOffsetIndex)=0, name:String:1:idx0(relativeOffset)=8:idx1(vlfOffsetIndex)=-1,]]
* at org.apache.geode.pdx.internal.WritablePdxInstanceImpl.setField(WritablePdxInstanceImpl.java:119)
* ...
*
* This code needs to create a {@literal new} {@link PdxInstance} from an existing {@link PdxInstance}
* or add the new (PDX) field to the PDX type metadata using {@code PdxType.addField(:PdxField)} before
* setting the new field on the {@link PdxInstance} using the {@link WritablePdxInstance}. Unfortunately,
* {@code PdxType} is part of the internal API and updating and ditributing a {@code PdxType} is complicated,
* requiring a Distributed Lock, among other responsibilities.
*/
@SuppressWarnings("unused")
protected @NonNull PdxInstance decorate(@NonNull PdxInstance pdxInstance) {
if (isMissingObjectTypeMetadata(pdxInstance)) {
String pdxInstanceClassName = pdxInstance.getClassName();
Assert.isTrue(hasValidClassName(pdxInstance), () ->
String.format("Class name [%s] is required and cannot be equal to [%s]",
pdxInstanceClassName, JSONFormatter.JSON_CLASSNAME));
pdxInstance = newPdxInstanceBuilder()
.copy(pdxInstance)
.writeString(AT_TYPE_METADATA_PROPERTY_NAME, pdxInstanceClassName)
.create();
}
return pdxInstance;
}
/**
* Constructs a new instance of {@link PdxInstanceBuilder}.
*
* @return a new instance of {@link PdxInstanceBuilder}; never {@literal null}.
* @see org.springframework.geode.pdx.PdxInstanceBuilder
*/
@NonNull PdxInstanceBuilder newPdxInstanceBuilder() {
return PdxInstanceBuilder.create();
}
/**
* Decorates the given {@link String JSON} to include the {@literal @type} metadata property in order to
* indicate the type of the {@literal JSON} object, which is required for deserialization back to PDX.
*
* If an {@link JsonProcessingException} is thrown during this operation and if the {@link PdxInstance}
* has a {@link #hasValidClassName(PdxInstance) valid class name}, then an attempt is made to serialize
* the {@link PdxInstance#getObject() object instance} of the {@link PdxInstance} to {@link String JSON}
* using Jackson's {@link ObjectMapper}.
*
* @param pdxInstance required {@link PdxInstance} from which the {@link String JSON} was serialized;
* must not be {@literal null}.
* @param json {@link String JSON} generated from the serialization of the {@link PdxInstance};
* must not be {@literal null}.
* @return the decorated {@link String JSON} including the {@literal @type} metadata property.
* @throws DataRetrievalFailureException if {@link String JSON} cannot be decorated with type metadata
* and the {@link PdxInstance} is not based on a valid {@link Class} type.
* @see JacksonObjectToJsonConverter#convert(Object)
* @see org.apache.geode.pdx.PdxInstance
* @see #newObjectMapper(Object)
*/
@SuppressWarnings("unused")
protected @NonNull String decorate(@NonNull PdxInstance pdxInstance, @NonNull String json) {
if (isDecorationRequired(pdxInstance, json)) {
try {
ObjectMapper objectMapper = newObjectMapper(json);
JsonNode jsonNode = objectMapper.readTree(json);
if (isMissingObjectTypeMetadata(jsonNode)) {
((ObjectNode) jsonNode).put(AT_TYPE_METADATA_PROPERTY_NAME, pdxInstance.getClassName());
json = objectMapper.writeValueAsString(jsonNode);
}
return json;
}
catch (JsonProcessingException cause) {
if (hasValidClassName(pdxInstance)) {
return convertPojoToJson(pdxInstance.getObject());
}
String message = String.format("Failed to parse JSON [%s]", json);
throw new DataRetrievalFailureException(message, cause);
}
}
return json;
}
/**
* Null-safe method to determine whether the given {@link PdxInstance}
* has a valid {@link Class#getName() Class Name}.
*
* @param pdxInstance {@link PdxInstance} to evaluate;
* @return a boolean value indicating whether the {@link PdxInstance}
* has a valid {@link Class#getName() Class Name}.
* @see org.apache.geode.pdx.PdxInstance
*/
boolean hasValidClassName(@Nullable PdxInstance pdxInstance) {
return Optional.ofNullable(pdxInstance)
.map(PdxInstance::getClassName)
.filter(StringUtils::hasText)
.filter(className -> !JSONFormatter.JSON_CLASSNAME.equals(className))
.isPresent();
}
private boolean isDecorationRequired(@Nullable PdxInstance pdxInstance, @Nullable String json) {
return isMissingObjectTypeMetadata(pdxInstance) && isValidJson(json);
}
private boolean isMissingObjectTypeMetadata(@Nullable JsonNode node) {
return isObjectNode(node) && !node.has(AT_TYPE_METADATA_PROPERTY_NAME);
}
private boolean isMissingObjectTypeMetadata(@Nullable PdxInstance pdxInstance) {
return pdxInstance != null && !pdxInstance.hasField(AT_TYPE_METADATA_PROPERTY_NAME);
}
/**
* Null-safe method to determine if the given {@link JsonNode} represents a valid {@link String JSON} object.
*
* @param node {@link JsonNode} to evaluate.
* @return a boolean valued indicating whether the given {@link JsonNode} is a valid {@link ObjectNode}.
* @see com.fasterxml.jackson.databind.node.ObjectNode
* @see com.fasterxml.jackson.databind.JsonNode
*/
boolean isObjectNode(@Nullable JsonNode node) {
return node != null && (node.isObject() || node instanceof ObjectNode);
}
/**
* Null-safe method to determine whether the given {@link String JSON} is valid.
*
* @param json {@link String} containing JSON to evaluate.
* @return a boolean value indicating whether the given {@link String JSON} is valid.
*/
boolean isValidJson(@Nullable String json) {
return StringUtils.hasText(json);
}
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright 2017-present 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.geode.data.json.converter.support;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.POJONode;
import org.springframework.core.convert.converter.Converter;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.data.mapping.MappingException;
import org.springframework.geode.data.json.converter.JsonToObjectConverter;
import org.springframework.geode.pdx.PdxInstanceWrapper;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* {@link JsonToObjectConverter} implementation using Jackson to convert {@link String JSON}
* to an {@link Object} (POJO).
*
* @author John Blum
* @see com.fasterxml.jackson.databind.JsonNode
* @see com.fasterxml.jackson.databind.ObjectMapper
* @see com.fasterxml.jackson.databind.node.POJONode
* @see org.springframework.core.convert.converter.Converter
* @see org.springframework.geode.data.json.converter.JsonToObjectConverter
* @since 1.3.0
*/
public class JacksonJsonToObjectConverter implements JsonToObjectConverter {
protected static final String AT_TYPE_FIELD_NAME = PdxInstanceWrapper.AT_TYPE_FIELD_NAME;
private ObjectMapper objectMapper = newObjectMapper();
/**
* Constructs a new Jackson {@link ObjectMapper} to convert {@link String JSON} into an {@link Object} (POJO).
*
* @return a new Jackson {@link ObjectMapper}; never {@literal null}.
* @see com.fasterxml.jackson.databind.ObjectMapper
*/
// TODO configure via an SPI
private @NonNull ObjectMapper newObjectMapper() {
return new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true)
.findAndRegisterModules();
}
/**
* Returns a reference to the configured Jackson {@link ObjectMapper} used by this {@link Converter}
* to convert {@link String JSON} into an {@link Object} (POJO).
*
* @return a reference to the configured Jackson {@link ObjectMapper}.
*/
protected @NonNull ObjectMapper getObjectMapper() {
return this.objectMapper;
}
/**
* Converts from {@link String JSON} to an {@link Object} (POJO) using Jackson's {@link ObjectMapper}.
*
* @param json {@link String} containing {@literal JSON} to convert.
* @return an {@link Object} (POJO) converted from the given {@link String JSON}.
* @see #getObjectMapper()
*/
@Override
public @Nullable Object convert(@Nullable String json) {
if (StringUtils.hasText(json)) {
String objectTypeName = null;
try {
ObjectMapper objectMapper = getObjectMapper();
JsonNode jsonNode = objectMapper.readTree(json);
if (isPojo(jsonNode)) {
return ((POJONode) jsonNode).getPojo();
}
else {
Assert.state(jsonNode.isObject(), () -> String.format("The JSON [%s] must be an object", json));
Assert.state(jsonNode.has(AT_TYPE_FIELD_NAME),
() -> String.format("The JSON object [%1$s] must have an '%2$s' metadata field",
json, AT_TYPE_FIELD_NAME));
objectTypeName = jsonNode.get(AT_TYPE_FIELD_NAME).asText();
Class<?> objectType =
ClassUtils.forName(objectTypeName, Thread.currentThread().getContextClassLoader());
return objectMapper.readValue(json, objectType);
}
}
catch (ClassNotFoundException cause) {
throw new MappingException(String.format("Failed to map JSON [%1$s] to an Object of type [%2$s]",
json, objectTypeName), cause);
}
catch (JsonProcessingException cause) {
throw new DataRetrievalFailureException(String.format("Failed to read JSON [%s]", json), cause);
}
}
return null;
}
/**
* Null-safe method to determine whether the given {@link JsonNode} represents a {@link Object POJO}.
*
* @param jsonNode {@link JsonNode} to evaluate.
* @return a boolean value indicating whether the given {@link JsonNode} represents a {@link Object POJO}.
* @see com.fasterxml.jackson.databind.JsonNode
*/
boolean isPojo(@Nullable JsonNode jsonNode) {
return jsonNode != null
&& (jsonNode instanceof POJONode || jsonNode.isPojo() || JsonNodeType.POJO.equals(jsonNode.getNodeType()));
}
}

View File

@@ -0,0 +1,149 @@
/*
* Copyright 2017-present 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.geode.data.json.converter.support;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.geode.pdx.PdxInstance;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.geode.data.json.converter.JsonToPdxArrayConverter;
import org.springframework.geode.data.json.converter.JsonToPdxConverter;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* The {@link JacksonJsonToPdxConverter} class is an implementation of the {@link JsonToPdxArrayConverter} that is
* capable of converting an array of {@literal JSON} objects into an array of {@link PdxInstance PdxInstances}.
*
* @author John Blum
* @see com.fasterxml.jackson.databind.JsonNode
* @see com.fasterxml.jackson.databind.ObjectMapper
* @see com.fasterxml.jackson.databind.node.ArrayNode
* @see com.fasterxml.jackson.databind.node.ObjectNode
* @see org.apache.geode.pdx.PdxInstance
* @see org.springframework.geode.data.json.converter.JsonToPdxArrayConverter
* @see org.springframework.geode.data.json.converter.JsonToPdxConverter
* @since 1.3.0
*/
public class JacksonJsonToPdxConverter implements JsonToPdxArrayConverter {
private JsonToPdxConverter converter = newJsonToPdxConverter();
private ObjectMapper objectMapper = newObjectMapper();
private @NonNull <T> Iterable<T> asIterable(@NonNull Iterator<T> iterator) {
return () -> iterator;
}
// TODO configure via an SPI
private JsonToPdxConverter newJsonToPdxConverter() {
return new JSONFormatterJsonToPdxConverter();
}
// TODO configure via an SPI
private ObjectMapper newObjectMapper() {
return new ObjectMapper();
}
/**
* Returns a reference to the configured {@link JsonToPdxConverter} used to convert from a single object,
* {@literal JSON} {@link String} to PDX (i.e. as a {@link PdxInstance}.
*
* @return a reference to the configured {@link JsonToPdxConverter}; never {@literal null}.
* @see org.springframework.geode.data.json.converter.JsonToPdxConverter
*/
protected @NonNull JsonToPdxConverter getJsonToPdxConverter() {
return this.converter;
}
/**
* Returns a reference to the configured Jackson {@link ObjectMapper}.
*
* @return a reference to the configured Jackson {@link ObjectMapper}; never {@literal null}.
* @see com.fasterxml.jackson.databind.ObjectMapper
*/
protected @NonNull ObjectMapper getObjectMapper() {
return this.objectMapper;
}
/**
* Converts the given {@link String JSON} containing multiple objects into an array of {@link PdxInstance} objects.
*
* @param json {@link String JSON} data to convert.
* @return an array of {@link PdxInstance} objects from the given {@link String JSON}.
* @throws IllegalStateException if the {@link String JSON} does not start with
* either a JSON array or a JSON object.
* @see org.apache.geode.pdx.PdxInstance
*/
@Nullable @Override
public PdxInstance[] convert(String json) {
try {
JsonNode jsonNode = getObjectMapper().readTree(json);
List<PdxInstance> pdxList = new ArrayList<>();
if (isArray(jsonNode)) {
ArrayNode arrayNode = (ArrayNode) jsonNode;
JsonToPdxConverter converter = getJsonToPdxConverter();
for (JsonNode object : asIterable(CollectionUtils.nullSafeIterator(arrayNode.elements()))) {
pdxList.add(converter.convert(object.toString()));
}
}
else if (isObject(jsonNode)) {
ObjectNode objectNode = (ObjectNode) jsonNode;
pdxList.add(getJsonToPdxConverter().convert(objectNode.toString()));
}
else {
String message = String.format("Unable to process JSON node of type [%s];"
+ " expected either an [%s] or an [%s]", jsonNode.getNodeType(),
JsonNodeType.OBJECT, JsonNodeType.ARRAY);
throw new IllegalStateException(message);
}
return pdxList.toArray(new PdxInstance[0]);
}
catch (JsonProcessingException cause) {
throw new DataRetrievalFailureException("Failed to read JSON content", cause);
}
}
private boolean isArray(@Nullable JsonNode node) {
return node != null && (node.isArray() || JsonNodeType.ARRAY.equals(node.getNodeType()));
}
private boolean isObject(@Nullable JsonNode node) {
return node != null && (node.isObject() || JsonNodeType.OBJECT.equals(node.getNodeType()));
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright 2017-present 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.geode.data.json.converter.support;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.geode.data.json.converter.ObjectToJsonConverter;
import org.springframework.geode.pdx.PdxInstanceWrapper;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
/**
* A {@link ObjectToJsonConverter} implementation using Jackson's {@link ObjectMapper} to convert
* from an {@link Object} to a {@literal JSON} {@link String}.
*
* @author John Blum
* @see com.fasterxml.jackson.annotation.JsonTypeInfo
* @see com.fasterxml.jackson.core.JsonGenerator
* @see com.fasterxml.jackson.databind.ObjectMapper
* @see com.fasterxml.jackson.databind.MapperFeature
* @see org.springframework.core.convert.TypeDescriptor
* @see org.springframework.geode.data.json.converter.ObjectToJsonConverter
* @see org.springframework.geode.pdx.PdxInstanceWrapper
* @since 1.3.0
*/
public class JacksonObjectToJsonConverter implements ObjectToJsonConverter {
protected static final String AT_TYPE_METADATA_PROPERTY_NAME = PdxInstanceWrapper.AT_TYPE_FIELD_NAME;
/**
* Converts the given {@link Object} into {@link String JSON}.
*
* @param source {@link Object} to convert into {@link String JSON}.
* @return {@link String JSON} generated from the given {@link Object} using Jackson's {@link ObjectMapper}.
* @throws IllegalArgumentException if {@link Object source} is {@literal null}.
* @throws ConversionFailedException if a {@link JsonProcessingException} is thrown or another error occurs
* while trying to convert the given {@link Object} to {@link String JSON}.
* @see com.fasterxml.jackson.databind.ObjectMapper
* @see #convertObjectToJson(Object)
*/
@Override
public @NonNull String convert(@NonNull Object source) {
Assert.notNull(source, "Source object to convert must not be null");
try {
return convertObjectToJson(source);
}
catch (JsonProcessingException cause) {
throw new ConversionFailedException(TypeDescriptor.forObject(source), TypeDescriptor.valueOf(String.class),
source, cause);
}
}
/**
* Converts the given {@link Object} into {@link String JSON}.
*
* @param source {@link Object} to convert to {@link String JSON}; must not be {@literal null}.
* @return {@link String JSON} generated from the given {@link Object}.
* @throws IllegalArgumentException if {@link Object source} is {@literal null}.
* @throws JsonProcessingException if the generation of {@link String JSON} from the given {@link Object}
* results in an error.
* @see com.fasterxml.jackson.databind.ObjectMapper#writeValueAsString(Object)
* @see #newObjectMapper(Object)
*/
protected @NonNull String convertObjectToJson(@NonNull Object source) throws JsonProcessingException {
Assert.notNull(source, "Source object to convert must not be null");
return newObjectMapper(source).writeValueAsString(source);
}
/**
* Constructs a new instance of the Jackson {@link ObjectMapper} class.
*
* @return a new instance of the Jackson {@link ObjectMapper} class.
* @see com.fasterxml.jackson.databind.ObjectMapper
*/
protected @NonNull ObjectMapper newObjectMapper(@NonNull Object target) {
Assert.notNull(target, "Target object must not be null");
return newObjectMapper()
.addMixIn(target.getClass(), ObjectTypeMetadataMixin.class)
.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true)
.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true)
.configure(SerializationFeature.INDENT_OUTPUT, true)
.findAndRegisterModules();
}
/**
* Constructs a new instance of Jackson's {@link ObjectMapper}.
*
* @return a new instance of Jackson's {@link ObjectMapper}; never {@literal null}.
* @see com.fasterxml.jackson.databind.ObjectMapper
*/
@NonNull ObjectMapper newObjectMapper() {
return new ObjectMapper();
}
@JsonTypeInfo(
use = JsonTypeInfo.Id.CLASS,
include = JsonTypeInfo.As.PROPERTY,
property = AT_TYPE_METADATA_PROPERTY_NAME
)
@SuppressWarnings("all")
interface ObjectTypeMetadataMixin { }
}

View File

@@ -0,0 +1,443 @@
/*
* Copyright 2017-present 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.geode.data.support;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.geode.cache.Region;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.Lifecycle;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.env.Environment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.data.gemfire.support.SmartLifecycleSupport;
import org.springframework.geode.core.io.ResourceReader;
import org.springframework.geode.core.io.ResourceResolver;
import org.springframework.geode.core.io.ResourceWriter;
import org.springframework.geode.data.CacheDataImporterExporter;
import org.springframework.geode.data.support.ResourceCapableCacheDataImporterExporter.ExportResourceResolver;
import org.springframework.geode.data.support.ResourceCapableCacheDataImporterExporter.ImportResourceResolver;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A {@link CacheDataImporterExporter} implementation using the {@literal Decorator Software Design Pattern} to wrap
* an existing {@link CacheDataImporterExporter} in order to {@literal decorate} the cache (i.e. {@link Region}) data
* import and export operations, making them Spring {@link ApplicationContext}, {@link Environment}, {@link Lifecycle},
* {@link ResourceLoader} aware and capable.
*
* This wrapper {@literal decorates} the Apache Geode cache {@link Region} data import operation enabling it
* to be configured {@link ImportLifecycle#EAGER eagerly}, after the {@link Region} bean as been initialized,
* or {@link ImportLifecycle#LAZY lazily}, once all beans have been fully initialized and the Spring
* {@link ApplicationContext} is refreshed.
*
* @author John Blum
* @see org.apache.geode.cache.Region
* @see org.springframework.context.ApplicationContext
* @see org.springframework.context.ApplicationContextAware
* @see org.springframework.context.EnvironmentAware
* @see org.springframework.context.Lifecycle
* @see org.springframework.context.ResourceLoaderAware
* @see org.springframework.core.env.Environment
* @see org.springframework.core.io.ResourceLoader
* @see org.springframework.data.gemfire.support.SmartLifecycleSupport
* @see org.springframework.geode.core.io.ResourceReader
* @see org.springframework.geode.core.io.ResourceResolver
* @see org.springframework.geode.core.io.ResourceWriter
* @see org.springframework.geode.data.CacheDataImporterExporter
* @see org.springframework.geode.data.support.ResourceCapableCacheDataImporterExporter.ExportResourceResolver
* @see org.springframework.geode.data.support.ResourceCapableCacheDataImporterExporter.ImportResourceResolver
* @see <a href="https://en.wikipedia.org/wiki/Decorator_pattern">Decorator Software Design Pattern</a>
* @since 1.3.0
*/
@SuppressWarnings("rawtypes")
public class LifecycleAwareCacheDataImporterExporter implements CacheDataImporterExporter,
ApplicationContextAware, EnvironmentAware, InitializingBean, ResourceLoaderAware, SmartLifecycleSupport {
protected static final int DEFAULT_IMPORT_PHASE = Integer.MIN_VALUE + 1000000;
protected static final String CACHE_DATA_IMPORT_LIFECYCLE_PROPERTY_NAME =
"spring.boot.data.gemfire.cache.data.import.lifecycle";
protected static final String CACHE_DATA_IMPORT_PHASE_PROPERTY_NAME =
"spring.boot.data.gemfire.cache.data.import.phase";
private final AtomicReference<ImportLifecycle> resolvedImportLifecycle = new AtomicReference<>(null);
private final AtomicReference<Integer> resolvedImportPhase = new AtomicReference<>(null);
private final CacheDataImporterExporter importerExporter;
private Environment environment;
private final Set<Region> regionsForImport = Collections.synchronizedSet(new HashSet<>());
/**
* Constructs a new instance of the {@link LifecycleAwareCacheDataImporterExporter} initialized with the given,
* target {@link CacheDataImporterExporter} that is wrapped by this implementation to decorate all cache import
* and export data operations in order to make them {@link Lifecycle} aware and capable.
*
* @param importerExporter {@link CacheDataImporterExporter} wrapped by this implementation to {@literal decorate}
* the cache data import/export operations to be {@link Lifecycle} aware and capable; must not be {@literal null}.
* @throws IllegalArgumentException if {@link CacheDataImporterExporter} is {@literal null}.
* @see org.springframework.geode.data.CacheDataImporterExporter
*/
public LifecycleAwareCacheDataImporterExporter(@NonNull CacheDataImporterExporter importerExporter) {
Assert.notNull(importerExporter, "The CacheDataImporterExporter to decorate must not be null");
this.importerExporter = importerExporter;
}
/**
* Initializes the wrapped {@link CacheDataImporterExporter} if the importer/exporter
* implements {@link InitializingBean}.
*
* @throws Exception if {@link CacheDataImporterExporter} initialization fails.
*/
@Override
public void afterPropertiesSet() throws Exception {
CacheDataImporterExporter importerExporter = getCacheDataImporterExporter();
if (importerExporter instanceof InitializingBean) {
((InitializingBean) importerExporter).afterPropertiesSet();
}
}
/**
* Configures a reference to the Spring {@link ApplicationContext}.
*
* @param applicationContext Spring {@link ApplicationContext} in which this component operates.
* @see org.springframework.context.ApplicationContext
*/
@Override
public void setApplicationContext(@Nullable ApplicationContext applicationContext) {
if (applicationContext != null) {
CacheDataImporterExporter importerExporter = getCacheDataImporterExporter();
if (importerExporter instanceof ApplicationContextAware) {
((ApplicationContextAware) importerExporter).setApplicationContext(applicationContext);
}
}
}
/**
* Returns a reference to the configured {@link CacheDataImporterExporter} wrapped by this {@link Lifecycle} aware
* and capable {@link CacheDataImporterExporter}.
*
* @return the {@link CacheDataImporterExporter} enhanced and used as the delegate for this {@link Lifecycle} aware
* and capable {@link CacheDataImporterExporter}; never {@literal null}.
*/
protected @NonNull CacheDataImporterExporter getCacheDataImporterExporter() {
return this.importerExporter;
}
/**
* Configures a reference to the {@link Environment} used to access configuration for the behavior of
* the cache data import.
*
* @param environment {@link Environment} used to access context specific configuration for the cache data import.
* @see org.springframework.core.env.Environment
*/
@Override
public void setEnvironment(@Nullable Environment environment) {
this.environment = environment;
if (environment != null) {
CacheDataImporterExporter importerExporter = getCacheDataImporterExporter();
if (importerExporter instanceof EnvironmentAware) {
((EnvironmentAware) importerExporter).setEnvironment(environment);
}
}
}
/**
* Returns an {@link Optional} reference to the configured {@link Environment} used to access configuration
* for the behavior of the cache data import.
*
* If a reference to {@link Environment} was not configured, then this method will return {@link Optional#empty()}.
*
* @return an {@link Optional} reference to the configured {@link Environment}, or {@link Optional#empty()} if no
* {@link Environment} was configured.
* @see org.springframework.core.env.Environment
* @see #setEnvironment(Environment)
* @see java.util.Optional
*/
protected Optional<Environment> getEnvironment() {
return Optional.ofNullable(this.environment);
}
/**
* Configures the {@link ExportResourceResolver} of the wrapped {@link CacheDataImporterExporter}
* if the {@link ExportResourceResolver} is not {@literal null} and the {@link CacheDataImporterExporter}
* is {@link Resource} capable.
*
* @param exportResourceResolver {@link ResourceResolver} used to resolve a {@link Resource} for {@literal export}.
* @see org.springframework.geode.data.support.ResourceCapableCacheDataImporterExporter.ExportResourceResolver
* @see #getCacheDataImporterExporter()
*/
@Autowired(required = false)
public void setExportResourceResolver(@Nullable ExportResourceResolver exportResourceResolver) {
if (exportResourceResolver != null) {
CacheDataImporterExporter importerExporter = getCacheDataImporterExporter();
if (importerExporter instanceof ResourceCapableCacheDataImporterExporter) {
((ResourceCapableCacheDataImporterExporter) importerExporter)
.setExportResourceResolver(exportResourceResolver);
}
}
}
/**
* Configures the {@link ImportResourceResolver} of the wrapped {@link CacheDataImporterExporter}
* if the {@link ImportResourceResolver} is not {@literal null} and the {@link CacheDataImporterExporter}
* is {@link Resource} capable.
*
* @param importResourceResolver {@link ResourceResolver} used to resolve a {@link Resource} for {@literal import}.
* @see org.springframework.geode.data.support.ResourceCapableCacheDataImporterExporter.ImportResourceResolver
* @see #getCacheDataImporterExporter()
*/
@Autowired(required = false)
public void setImportResourceResolver(@Nullable ImportResourceResolver importResourceResolver) {
if (importResourceResolver != null) {
CacheDataImporterExporter importerExporter = getCacheDataImporterExporter();
if (importerExporter instanceof ResourceCapableCacheDataImporterExporter) {
((ResourceCapableCacheDataImporterExporter) importerExporter)
.setImportResourceResolver(importResourceResolver);
}
}
}
/**
* @inheritDoc
*/
@Override
public int getPhase() {
return resolveImportPhase();
}
/**
* Returns the {@link Set} of {@link Region Regions} to import data into.
*
* @return a {@link Set} of {@link Region Regions} to evaluate on import; never {@literal null}.
* @see org.apache.geode.cache.Region
* @see java.util.Set
*/
@NonNull Set<Region> getRegionsForImport() {
return this.regionsForImport;
}
/**
* @inheritDoc
*/
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
if (resourceLoader != null) {
CacheDataImporterExporter importerExporter = getCacheDataImporterExporter();
if (importerExporter instanceof ResourceLoaderAware) {
((ResourceLoaderAware) importerExporter).setResourceLoader(resourceLoader);
}
}
}
/**
* Configures the {@link ResourceReader} of the wrapped {@link CacheDataImporterExporter}
* if the {@link ResourceReader} is not {@literal null} and the {@link CacheDataImporterExporter}
* is {@link Resource} capable.
*
* @param resourceReader {@link ResourceReader} used to read data from a {@link Resource} on {@literal import}.
* @see org.springframework.geode.core.io.ResourceReader
* @see #getCacheDataImporterExporter()
*/
@Autowired(required = false)
public void setResourceReader(@Nullable ResourceReader resourceReader) {
if (resourceReader != null) {
CacheDataImporterExporter importerExporter = getCacheDataImporterExporter();
if (importerExporter instanceof ResourceCapableCacheDataImporterExporter) {
((ResourceCapableCacheDataImporterExporter) importerExporter).setResourceReader(resourceReader);
}
}
}
/**
* Configures the {@link ResourceWriter} of the wrapped {@link CacheDataImporterExporter}
* if the {@link ResourceWriter} is not {@literal null} and the {@link CacheDataImporterExporter}
* is {@link Resource} capable.
*
* @param resourceWriter {@link ResourceWriter} used to write data to a {@link Resource} on {@literal export}.
* @see org.springframework.geode.core.io.ResourceWriter
* @see #getCacheDataImporterExporter()
*/
@Autowired(required = false)
public void setResourceWriter(@Nullable ResourceWriter resourceWriter) {
if (resourceWriter != null) {
CacheDataImporterExporter importerExporter = getCacheDataImporterExporter();
if (importerExporter instanceof ResourceCapableCacheDataImporterExporter) {
((ResourceCapableCacheDataImporterExporter) importerExporter).setResourceWriter(resourceWriter);
}
}
}
/**
* @inheritDoc
*/
@NonNull @Override
public Region exportFrom(@NonNull Region region) {
return getCacheDataImporterExporter().exportFrom(region);
}
/**
* @inheritDoc
*/
@NonNull @Override
public Region importInto(@NonNull Region region) {
if (resolveImportLifecycle().isEager()) {
return getCacheDataImporterExporter().importInto(region);
}
else {
getRegionsForImport().add(region);
return region;
}
}
/**
* Resolves the configured {@link ImportLifecycle}.
*
* The cache data import lifecycle is configured with the
* {@literal spring.boot.data.gemfire.cache.data.import.lifecycle} property
* in Spring Boot {@literal application.properties}.
*
* @return the configured {@link ImportLifecycle}.
* @see LifecycleAwareCacheDataImporterExporter.ImportLifecycle
*/
protected ImportLifecycle resolveImportLifecycle() {
return resolvedImportLifecycle.updateAndGet(currentValue -> currentValue != null ? currentValue
: getEnvironment()
.map(env -> env.getProperty(CACHE_DATA_IMPORT_LIFECYCLE_PROPERTY_NAME, String.class,
ImportLifecycle.getDefault().name()))
.map(ImportLifecycle::from)
.orElseGet(ImportLifecycle::getDefault));
}
/**
* Resolves the configured {@link SmartLifecycleSupport#getPhase() SmartLifecycle Phase} in which the cache data
* import will be performed.
*
* @return the configured {@link SmartLifecycleSupport#getPhase() SmartLifecycle Phase}.
* @see #getPhase()
*/
protected int resolveImportPhase() {
return resolvedImportPhase.updateAndGet(currentValue -> currentValue != null ? currentValue
: getEnvironment()
.map(env -> env.getProperty(CACHE_DATA_IMPORT_PHASE_PROPERTY_NAME, Integer.class, DEFAULT_IMPORT_PHASE))
.orElse(DEFAULT_IMPORT_PHASE));
}
/**
* Performs the cache data import for each of the targeted {@link Region Regions}.
*
* @see #getCacheDataImporterExporter()
* @see #getRegionsForImport()
*/
@Override
public void start() {
// Technically, the resolveImportLifecycle().isLazy() check is not strictly required since if the cache data
// import is "eager", then the regionsForImport Set will be empty anyway.
if (resolveImportLifecycle().isLazy()) {
getRegionsForImport().forEach(getCacheDataImporterExporter()::importInto);
}
}
/**
* An {@link Enum Enumeration} defining the different modes for the cache data import lifecycle.
*/
public enum ImportLifecycle {
EAGER("Imports cache data during Region bean post processing, after initialization"),
LAZY("Imports cache data during the appropriate phase on Lifecycle start");
private final String description;
ImportLifecycle(@NonNull String description) {
Assert.hasText(description, "The enumerated value must have a description");
this.description = description;
}
public static @NonNull ImportLifecycle getDefault() {
return LAZY;
}
public static @Nullable ImportLifecycle from(String name) {
for (ImportLifecycle importCycle : values()) {
if (importCycle.name().equalsIgnoreCase(name)) {
return importCycle;
}
}
return null;
}
public boolean isEager() {
return EAGER.equals(this);
}
public boolean isLazy() {
return LAZY.equals(this);
}
@Override
public String toString() {
return this.description;
}
}
}

View File

@@ -0,0 +1,713 @@
/*
* Copyright 2017-present 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.geode.data.support;
import static org.springframework.geode.core.util.ObjectUtils.initialize;
import java.io.File;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.apache.geode.cache.GemFireCache;
import org.apache.geode.cache.Region;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.expression.BeanFactoryAccessor;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.env.Environment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.spel.SpelCompilerMode;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.DataBindingPropertyAccessor;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.geode.core.env.EnvironmentMapAdapter;
import org.springframework.geode.core.io.ResourceReader;
import org.springframework.geode.core.io.ResourceResolver;
import org.springframework.geode.core.io.ResourceWriter;
import org.springframework.geode.core.io.support.ByteArrayResourceReader;
import org.springframework.geode.core.io.support.FileResourceWriter;
import org.springframework.geode.core.io.support.ResourceLoaderResourceResolver;
import org.springframework.geode.core.io.support.ResourcePrefix;
import org.springframework.geode.core.io.support.ResourceUtils;
import org.springframework.geode.core.util.ObjectAwareUtils;
import org.springframework.geode.data.AbstractCacheDataImporterExporter;
import org.springframework.geode.data.CacheDataImporterExporter;
import org.springframework.geode.expression.SmartEnvironmentAccessor;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An {@link AbstractCacheDataImporterExporter} extension and implementation capable of handling and managing import
* and export {@link Resource Resources}.
*
* @author John Blum
* @see java.io.File
* @see org.apache.geode.cache.GemFireCache
* @see org.apache.geode.cache.Region
* @see org.springframework.beans.factory.InitializingBean
* @see org.springframework.context.ApplicationContext
* @see org.springframework.context.ConfigurableApplicationContext
* @see org.springframework.context.ResourceLoaderAware
* @see org.springframework.expression.Expression
* @see org.springframework.expression.ExpressionParser
* @see org.springframework.expression.spel.SpelParserConfiguration
* @see org.springframework.expression.spel.standard.SpelExpressionParser
* @see org.springframework.core.io.Resource
* @see org.springframework.core.io.ResourceLoader
* @see org.springframework.geode.core.io.ResourceReader
* @see org.springframework.geode.core.io.ResourceResolver
* @see org.springframework.geode.core.io.ResourceWriter
* @see org.springframework.geode.data.AbstractCacheDataImporterExporter
* @since 1.3.1
*/
@SuppressWarnings("unused")
public abstract class ResourceCapableCacheDataImporterExporter extends AbstractCacheDataImporterExporter
implements InitializingBean, ResourceLoaderAware {
protected static final String CACHE_DATA_EXPORT_RESOURCE_LOCATION_PROPERTY_NAME =
"spring.boot.data.gemfire.cache.data.export.resource.location";
protected static final String CACHE_DATA_IMPORT_RESOURCE_LOCATION_PROPERTY_NAME =
"spring.boot.data.gemfire.cache.data.import.resource.location";
protected static final String RESOURCE_NAME_PATTERN = "data-%s.json";
private ExportResourceResolver exportResourceResolver;
private ImportResourceResolver importResourceResolver;
private ResourceLoader resourceLoader;
private ResourceReader resourceReader;
private ResourceWriter resourceWriter;
/**
* Initializes the export and import {@link ResourceResolver ResourceResolvers} as needed along with
* the {@link ResourceReader reader} and {@link ResourceWriter writer} for the {@link Resource}
* used on import and export.
*/
@Override
public void afterPropertiesSet() {
setExportResourceResolver(initialize(getExportResourceResolver(), FileSystemExportResourceResolver::new));
setImportResourceResolver(initialize(getImportResourceResolver(), ClassPathImportResourceResolver::new));
setResourceReader(initialize(getResourceReader(), ByteArrayResourceReader::new));
setResourceWriter(initialize(getResourceWriter(), FileResourceWriter::new));
Stream.of(getExportResourceResolver(), getImportResourceResolver())
.forEach(this.newCompositeObjectAwareInitializer());
}
Consumer<Object> newCompositeObjectAwareInitializer() {
return ObjectAwareUtils.applicationContextAwareObjectInitializer(getApplicationContext().orElse(null))
.andThen(ObjectAwareUtils.environmentAwareObjectInitializer(getEnvironment().orElse(null)))
.andThen(ObjectAwareUtils.resourceLoaderAwareObjectInitializer(getResourceLoader().orElse(null)));
}
/**
* Sets a reference to the configured {@link ExportResourceResolver}.
*
* @param exportResourceResolver configured {@link ExportResourceResolver} used by this importer/exporter
* to resolve {@link Resource Resources} on export.
* @see ExportResourceResolver
*/
@Autowired(required = false)
public void setExportResourceResolver(@Nullable ExportResourceResolver exportResourceResolver) {
this.exportResourceResolver = exportResourceResolver;
}
/**
* Gets the configured reference to the {@link ExportResourceResolver}.
*
* The configured {@link ExportResourceResolver} is guaranteed to be {@literal non-null} only if the
* {@link #afterPropertiesSet()} initialization method was called after construction of this importer/exporter.
* This is definitely true in a Spring context.
*
* @return the configured reference to the {@link ExportResourceResolver}.
* @see ExportResourceResolver
*/
protected @NonNull ExportResourceResolver getExportResourceResolver() {
return this.exportResourceResolver;
}
/**
* Sets a reference to the configured {@link ImportResourceResolver}.
*
* @param importResourceResolver configured {@link ImportResourceResolver} used by this importer/exporter
* to resolve {@link Resource Resources} on import.
* @see ImportResourceResolver
*/
@Autowired(required = false)
public void setImportResourceResolver(@Nullable ImportResourceResolver importResourceResolver) {
this.importResourceResolver = importResourceResolver;
}
/**
* Gets the configured reference to the {@link ImportResourceResolver}.
*
* The configured {@link ImportResourceResolver} is guaranteed to be {@literal non-null} only if the
* {@link #afterPropertiesSet()} initialization method was called after construction of this importer/exporter.
* This is definitely true in a Spring context.
*
* @return the configured reference to the {@link ImportResourceResolver}.
* @see ImportResourceResolver
*/
protected @NonNull ImportResourceResolver getImportResourceResolver() {
return this.importResourceResolver;
}
/**
* Configures the {@link ResourceLoader} used by this {@link CacheDataImporterExporter} to resolve
* and load {@link Resource Resources}.
*
* @param resourceLoader {@link ResourceLoader} used to resolve and load {@link Resource Resources}.
* @see org.springframework.core.io.ResourceLoader
*/
@Override
public void setResourceLoader(@Nullable ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
/**
* Returns an {@link Optional} reference to the configured {@link ResourceLoader}
* used to load {@link Resource Resources}.
*
* @return an {@link Optional} reference to the configured {@link ResourceLoader}.
* @see org.springframework.core.io.ResourceLoader
* @see java.util.Optional
*/
protected Optional<ResourceLoader> getResourceLoader() {
return Optional.ofNullable(this.resourceLoader);
}
/**
* Sets a reference to the configured {@link ResourceReader}.
*
* @param resourceReader configured {@link ResourceReader} used by this importer/exporter
* to read from a {@link Resource} on import.
* @see org.springframework.geode.core.io.ResourceReader
*/
@Autowired(required = false)
public void setResourceReader(@Nullable ResourceReader resourceReader) {
this.resourceReader = resourceReader;
}
/**
* Gets the configured {@link ResourceReader} used to read data from a {@link Resource} on {@literal import}.
*
* The configured {@link ResourceReader} is guaranteed to be {@literal non-null} only if the
* {@link #afterPropertiesSet()} initialization method was called after construction of this importer/exporter.
* This is definitely true in a Spring context.
*
* @return the configured {@link ResourceReader}.
* @see org.springframework.geode.core.io.ResourceReader
*/
protected @NonNull ResourceReader getResourceReader() {
return this.resourceReader;
}
/**
* Set a reference to the configured {@link ResourceWriter}.
*
* @param resourceWriter configured {@link ResourceWriter} used by this importer/exporter
* to write to a {@link Resource} on export.
* @see org.springframework.geode.core.io.ResourceWriter
*/
@Autowired(required = false)
public void setResourceWriter(@Nullable ResourceWriter resourceWriter) {
this.resourceWriter = resourceWriter;
}
/**
* Gets the configured {@link ResourceWriter} used to write data to the {@link Resource} on {@literal export}.
*
* The configured {@link ResourceWriter} is guaranteed to be {@literal non-null} only if the
* {@link #afterPropertiesSet()} initialization method was called after construction of this importer/exporter.
* This is definitely true in a Spring context.
*
* @return the configured {@link ResourceWriter}.
* @see org.springframework.geode.core.io.ResourceWriter
*/
protected @NonNull ResourceWriter getResourceWriter() {
return this.resourceWriter;
}
/**
* {@link ResourceResolver} interface extension used to resolve {@link GemFireCache cache}
* {@link Resource Resources}.
*
* @see org.springframework.geode.core.io.ResourceResolver
* @see org.apache.geode.cache.GemFireCache
* @see org.apache.geode.cache.Region
*/
@FunctionalInterface
protected interface CacheResourceResolver extends ResourceResolver {
/**
* Tries to resolve a {@link Resource} to a {@link String location} containing data for the given {@link Region}.
* The {@link Region} is used to determine the {@link String location} of the {@link Resource} to load.
*
* @param region {@link Region} used to resolve the {@link Resource}.
* @return an {@link Optional} {@link Resource} handle to a {@link String location} containing data
* for the given {@link Region}.
* @see org.springframework.core.io.Resource
* @see org.apache.geode.cache.Region
* @see java.util.Optional
*/
Optional<Resource> resolve(@NonNull Region<?, ?> region);
/**
* @inheritDoc
*/
@Override
default Optional<Resource> resolve(@NonNull String location) {
return Optional.empty();
}
}
/**
* Abstract base class containing functionality common to all {@link GemFireCache cache} based
* {@link ResourceResolver ResourceResolvers}, whether for import or export.
*
* @see org.springframework.geode.core.io.support.ResourceLoaderResourceResolver
* @see org.springframework.context.ApplicationContextAware
* @see org.springframework.context.EnvironmentAware
* @see CacheResourceResolver
*/
protected static abstract class AbstractCacheResourceResolver extends ResourceLoaderResourceResolver
implements ApplicationContextAware, CacheResourceResolver, EnvironmentAware {
private ApplicationContext applicationContext;
private Environment environment;
private final ExpressionParser expressionParser;
private final Logger logger = LoggerFactory.getLogger(getClass());
private final Map<String, Expression> compiledExpressions;
private final SimpleEvaluationContext.Builder evaluationContextBuilder;
/**
* Constructs a new instance of {@link AbstractCacheResourceResolver}.
*
* This constructor initializes the SpEL objects used to parse and evaluate SpEL expressions in order to
* fully qualify and resolve {@link Resource} {@link String locations} defined as properties
* in Spring Boot {@literal application.properties} for Import and Export {@link Resource Resources}.
*
* @see #newExpressionParser()
* @see #newEvaluationContextBuilder()
*/
public AbstractCacheResourceResolver() {
this.expressionParser = newExpressionParser();
this.evaluationContextBuilder = newEvaluationContextBuilder();
this.compiledExpressions = new ConcurrentHashMap<>();
}
private ExpressionParser newExpressionParser() {
ClassLoader classLoader = getApplicationContext()
.map(ApplicationContext::getClassLoader)
.orElseGet(ClassUtils::getDefaultClassLoader);
return new SpelExpressionParser(new SpelParserConfiguration(SpelCompilerMode.MIXED, classLoader));
}
private SimpleEvaluationContext.Builder newEvaluationContextBuilder() {
PropertyAccessor[] propertyAccessors = {
new BeanFactoryAccessor(),
DataBindingPropertyAccessor.forReadOnlyAccess(),
SmartEnvironmentAccessor.create()
};
SimpleEvaluationContext.Builder builder = SimpleEvaluationContext.forPropertyAccessors(propertyAccessors)
.withInstanceMethods();
String conversionServiceBeanName = ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME;
return getApplicationContext()
.filter(applicationContext -> applicationContext.containsBean(conversionServiceBeanName))
.map(applicationContext -> applicationContext.getBean(conversionServiceBeanName, ConversionService.class))
.map(builder::withConversionService)
.orElse(builder);
}
/**
* Constructs a new {@link EvaluationContext} used during the evaluation of SpEL {@link String expressions}.
*
* @return a new {@link EvaluationContext}; never {@literal null}.
* @see org.springframework.expression.EvaluationContext
*/
protected @NonNull EvaluationContext newEvaluationContext() {
return this.evaluationContextBuilder.build();
}
/**
* Configures a reference to the Spring {@link ApplicationContext}.
*
* @param applicationContext reference to the {@link ApplicationContext}.
* @see org.springframework.context.ApplicationContext
*/
@Override
public void setApplicationContext(@Nullable ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/**
* Returns an {@link Optional} reference to a Spring {@link ApplicationContext}.
*
* @return an {@link Optional} reference to a Spring {@link ApplicationContext}.
* @see org.springframework.context.ApplicationContext
* @see java.util.Optional
*/
protected Optional<ApplicationContext> getApplicationContext() {
return Optional.ofNullable(this.applicationContext);
}
/**
* Configures a reference to the Spring {@link Environment}.
*
* @param environment reference to the {@link Environment}.
* @see org.springframework.core.env.Environment
*/
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
/**
* Returns an {@link Optional} reference to the Spring {@link Environment}.
*
* @return an {@link Optional} reference to the Spring {@link Environment}.
* @see org.springframework.core.env.Environment
* @see java.util.Optional
*/
protected Optional<Environment> getEnvironment() {
return Optional.ofNullable(this.environment);
}
/**
* Gets the configured {@link ExpressionParser} used to parse SpEL {@link String expressions}.
*
* @return the configured {@link ExpressionParser}; never {@literal null}.
* @see org.springframework.expression.ExpressionParser
*/
protected @NonNull ExpressionParser getExpressionParser() {
return this.expressionParser;
}
/**
* Return the configured {@link Logger} to log messages.
*
* @return the configured {@link Logger}.
* @see org.slf4j.Logger
*/
protected Logger getLogger() {
return this.logger;
}
/**
* Gets the configured {@link ParserContext} used by the {@link ExpressionParser} to identify SpEL expressions.
*
* @return the configured {@link ParserContext}.
* @see org.springframework.expression.ParserContext
*/
protected @NonNull ParserContext getParserContext() {
return ParserContext.TEMPLATE_EXPRESSION;
}
/**
* @inheritDoc
*/
@Override
protected boolean isQualified(@Nullable Resource resource) {
return super.isQualified(resource) && resource.exists();
}
/**
* Determines a fully-qualified {@link String resource location} for the given {@link Region}.
*
* @param region {@link Region} to evaluate; must not be {@literal null}.
* @return a fully-qualified {@link String resource location} for the given {@link Region}.
* @see Region
* @see #getResourceName(Region)
* @see #getResourcePath()
*/
protected @NonNull String getFullyQualifiedResourceLocation(@NonNull Region<?, ?> region) {
return String.format("%1$s%2$s", getResourcePath(), getResourceName(region));
}
/**
* Determines the {@link String location} of a {@link Resource} for the given {@link Region}.
*
* @param region {@link Region} used to locate the desired {@link Resource}; must not be {@literal null}.
* @return a {@link Resource} {@link String location} for the given {@link Region}.
* @throws IllegalArgumentException if {@link Region} is {@literal null}.
* @see org.apache.geode.cache.Region
*/
protected @NonNull String getResourceLocation(@NonNull Region<?, ?> region, @NonNull String propertyName) {
Assert.notNull(region, "Region must not be null");
Assert.hasText(propertyName, () -> String.format("Property name [%s] must be specified", propertyName));
return getEnvironment()
.filter(environment -> environment.containsProperty(propertyName))
.map(environment -> environment.getProperty(propertyName))
.filter(StringUtils::hasText)
.map(resourceLocation -> evaluate(resourceLocation, region))
.orElseGet(() -> getFullyQualifiedResourceLocation(region));
}
/**
* Evaluates the given SpEL {@link String expression}.
*
* @param expressionString {@link String} containing the SpEL expression to evaluate; must not be {@literal null}.
* @param region {@link Region} used to resolve {@literal regionName} variable references
* in the {@link String expression}; must not be {@literal null}.
* @return the value of the evaluated {@link String expression}.
* @see org.springframework.expression.Expression#getValue(EvaluationContext, Object)
* @see org.apache.geode.cache.Region
* @see #parse(String)
*/
protected @Nullable String evaluate(@NonNull String expressionString, @NonNull Region<?, ?> region) {
EvaluationContext evaluationContext = newEvaluationContext();
evaluationContext.setVariable("regionName", region.getName().toLowerCase());
getEnvironment().ifPresent(environment ->
evaluationContext.setVariable("env", EnvironmentMapAdapter.from(environment)));
Expression expression = parse(expressionString);
Object value = getApplicationContext()
.map(applicationContext -> expression.getValue(evaluationContext, applicationContext))
.orElseGet(() -> expression.getValue(evaluationContext));
return value != null ? value.toString() : null;
}
/**
* Parses the given {@link String expressionString}.
*
* This method will cache parsed {@link Expression Expressions} to speed up the evaluation process.
*
* @param expressionString {@link String} containing the SpEL expression to parse.
* @return an {@link Expression} object parsed from the given {@link String expression}.
* @see org.springframework.expression.ExpressionParser#parseExpression(String, ParserContext)
* @see org.springframework.expression.Expression
* @see #getExpressionParser()
* @see #getParserContext()
*/
protected Expression parse(String expressionString) {
return this.compiledExpressions.computeIfAbsent(expressionString,
it -> getExpressionParser().parseExpression(it, getParserContext()));
}
/**
* Determines a {@link String resource name} for the given {@link Region}.
*
* The default implementation bases the {@link String resource name} on
* the {@link Region#getName() Region's lowercase name}.
*
* @param region {@link Region} to evaluate; must not be {@literal null}.
* @return a {@link String resource name} for the given {@link Region}.
* @see Region
* @see #getResourceName(String)
*/
protected @NonNull String getResourceName(@NonNull Region<?, ?> region) {
return getResourceName(region.getName().toLowerCase());
}
/**
* Determines a {@link String resource name} for the given {@link String name}.
*
* @param name {@link String} containing the name to evaluate; must not be {@literal null}.
* @return a {@link String resource name} from the given {@link String name}.
*/
protected @NonNull String getResourceName(@NonNull String name) {
return String.format(RESOURCE_NAME_PATTERN, name);
}
/**
* Get the {@link String base path} for the targeted {@link Resource}.
*
* @return the {@link String base path} for the targeted {@link Resource}.
*/
protected abstract @NonNull String getResourcePath();
}
/**
* Marker interface extending {@link CacheResourceResolver} for cache data exports.
*
* @see org.springframework.geode.core.io.ResourceResolver
* @see CacheResourceResolver
*/
@FunctionalInterface
public interface ExportResourceResolver extends CacheResourceResolver { }
/**
* Abstract base class extended by export {@link CacheResourceResolver} implementations, providing a template
* to resolve the {@link Resource} used for export.
*
* @see AbstractCacheResourceResolver
* @see ExportResourceResolver
*/
public static abstract class AbstractExportResourceResolver extends AbstractCacheResourceResolver
implements ExportResourceResolver {
/**
* @inheritDoc
*/
@Override
public Optional<Resource> resolve(@NonNull Region<?, ?> region) {
Assert.notNull(region, "Region must not be null");
String resourceLocation = getResourceLocation(region, CACHE_DATA_EXPORT_RESOURCE_LOCATION_PROPERTY_NAME);
Optional<Resource> resource = resolve(resourceLocation);
boolean writable = resource.filter(ResourceUtils::isWritable).isPresent();
if (!writable) {
getLogger().warn("Resource [{}] for Region [{}] is not writable",
resourceLocation, region.getFullPath());
}
return resource;
}
/**
* @inheritDoc
*/
@Override
protected @Nullable Resource onMissingResource(@Nullable Resource resource, @NonNull String location) {
getLogger().warn("Resource [{}] at location [{}] does not exist; will try to create it on export",
ResourceUtils.nullSafeGetDescription(resource), location);
return resource;
}
}
/**
* Resolves the {@link Resource} used for {@literal export} from the {@literal filesystem}.
*/
public static class FileSystemExportResourceResolver extends AbstractExportResourceResolver {
@Override
protected @NonNull String getResourcePath() {
return String.format("%1$s%2$s%3$s", ResourcePrefix.FILESYSTEM_URL_PREFIX.toUrlPrefix(),
System.getProperty("user.dir"), File.separator);
}
}
/**
* Marker interface extending {@link CacheResourceResolver} for cache data imports.
*
* @see org.springframework.geode.core.io.ResourceResolver
* @see CacheResourceResolver
*/
@FunctionalInterface
public interface ImportResourceResolver extends CacheResourceResolver { }
/**
* Abstract base class extended by import {@link ResourceResolver} implementations, providing a template
* to resolve the {@link Resource} to import.
*
* @see AbstractCacheResourceResolver
* @see ImportResourceResolver
*/
public static abstract class AbstractImportResourceResolver extends AbstractCacheResourceResolver
implements ImportResourceResolver {
/**
* @inheritDoc
*/
@Override
public Optional<Resource> resolve(@NonNull Region<?, ?> region) {
Assert.notNull(region, "Region must not be null");
String resourceLocation = getResourceLocation(region, CACHE_DATA_IMPORT_RESOURCE_LOCATION_PROPERTY_NAME);
Optional<Resource> resource = resolve(resourceLocation);
boolean exists = resource.isPresent();
boolean readable = exists && resource.filter(Resource::isReadable).isPresent();
if (!exists) {
getLogger().warn("Resource [{}] for Region [{}] could not be found; skipping import for Region",
resourceLocation, region.getFullPath());
}
else {
Assert.state(readable, () -> String.format("Resource [%1$s] for Region [%2$s] is not readable",
resourceLocation, region.getFullPath()));
}
return resource;
}
@Nullable @Override
protected Resource onMissingResource(@Nullable Resource resource, @NonNull String location) {
getLogger().warn("Resource [{}] at location [{}] does not exist; skipping import",
ResourceUtils.nullSafeGetDescription(resource), location);
return null;
}
}
/**
* Resolves the {@link Resource} to {@literal import} from the {@literal classpath}.
*/
public static class ClassPathImportResourceResolver extends AbstractImportResourceResolver {
@Override
protected @NonNull String getResourcePath() {
return ResourcePrefix.CLASSPATH_URL_PREFIX.toUrlPrefix();
}
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2017-present 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.geode.distributed.event;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.DistributedSystem;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.geode.distributed.event.support.MemberDepartedEvent;
import org.springframework.geode.distributed.event.support.MemberJoinedEvent;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
/**
* The {@link ApplicationContextMembershipListener} class is an extension of {@link MembershipListenerAdapter} used to
* adapt the {@link ConfigurableApplicationContext} to handle and process {@link MembershipEvent membership events},
* and specifically {@link MemberDepartedEvent} and {@link MemberJoinedEvent}, by
* {@link ConfigurableApplicationContext#close() closing} and {@link ConfigurableApplicationContext#refresh() refreshing}
* the {@link ConfigurableApplicationContext} when the {@link DistributedMember peer member} departs and joins the
* {@link DistributedSystem cluster}.
*
* @author John Blum
* @see org.apache.geode.distributed.DistributedMember
* @see org.apache.geode.distributed.DistributedSystem
* @see org.springframework.context.ConfigurableApplicationContext
* @see org.springframework.geode.distributed.event.support.MemberDepartedEvent
* @see org.springframework.geode.distributed.event.support.MemberJoinedEvent
* @since 1.3.0
*/
public class ApplicationContextMembershipListener
extends MembershipListenerAdapter<ApplicationContextMembershipListener> {
private final ConfigurableApplicationContext applicationContext;
/**
* Constructs a new instance of {@link ConfigurableApplicationContext} initialized with
* the given {@link ConfigurableApplicationContext}.
*
* @param applicationContext configured {@link ConfigurableApplicationContext}; must not be {@literal null}.
* @throws IllegalArgumentException if {@link ConfigurableApplicationContext} is {@literal null}.
* @see org.springframework.context.ConfigurableApplicationContext
*/
public ApplicationContextMembershipListener(@NonNull ConfigurableApplicationContext applicationContext) {
Assert.notNull(applicationContext, "ConfigurableApplicationContext must not be null");
this.applicationContext = applicationContext;
}
/**
* Returns a reference to the configured {@link ConfigurableApplicationContext}.
*
* @return a reference to the configured {@link ConfigurableApplicationContext}.
* @see org.springframework.context.ConfigurableApplicationContext
*/
protected @NonNull ConfigurableApplicationContext getApplicationContext() {
return this.applicationContext;
}
/**
* Handles the {@link MembershipEvent membership event} when a {@link DistributedMember peer member}
* departs from the {@link DistributedSystem cluster} by calling {@link ConfigurableApplicationContext#close()}.
*
* @param event {@link MemberDepartedEvent} to handle.
* @see org.springframework.geode.distributed.event.support.MemberDepartedEvent
* @see org.springframework.context.ConfigurableApplicationContext#close()
*/
@Override
public void handleMemberDeparted(MemberDepartedEvent event) {
getApplicationContext().close();
}
/**
* Handles the {@link MembershipEvent membership event} when a {@link DistributedMember peer member}
* joins the {@link DistributedSystem cluster} by calling {@link ConfigurableApplicationContext#refresh()}.
*
* @param event {@link MemberJoinedEvent} to handle.
* @see org.springframework.geode.distributed.event.support.MemberJoinedEvent
* @see org.springframework.context.ConfigurableApplicationContext#refresh()
*/
@Override
public void handleMemberJoined(MemberJoinedEvent event) {
getApplicationContext().refresh();
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2017-present 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.geode.expression;
import java.util.Optional;
import org.springframework.core.env.Environment;
import org.springframework.core.env.EnvironmentCapable;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypedValue;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* Spring {@link PropertyAccessor} implementation that knows how to access properties
* from {@link Environment} and {@link EnvironmentCapable} objects.
*
* @author John Blum
* @see org.springframework.core.env.Environment
* @see org.springframework.core.env.EnvironmentCapable
* @see org.springframework.expression.PropertyAccessor
* @since 1.3.1
*/
public class SmartEnvironmentAccessor implements PropertyAccessor {
/**
* Factory method used to construct a new instance of {@link SmartEnvironmentAccessor}.
*
* @return a new instance of {@link SmartEnvironmentAccessor}.
*/
public static @NonNull SmartEnvironmentAccessor create() {
return new SmartEnvironmentAccessor();
}
/**
* @inheritDoc
*/
@Nullable @Override
public Class<?>[] getSpecificTargetClasses() {
return new Class[] { Environment.class, EnvironmentCapable.class };
}
private Optional<Environment> asEnvironment(@Nullable Object target) {
Environment environment = target instanceof Environment ? (Environment) target
: target instanceof EnvironmentCapable ? ((EnvironmentCapable) target).getEnvironment()
: null;
return Optional.ofNullable(environment);
}
/**
* @inheritDoc
*/
@Override
public boolean canRead(EvaluationContext context, @Nullable Object target, String name) {
return asEnvironment(target)
.filter(environment -> environment.containsProperty(name))
.isPresent();
}
/**
* @inheritDoc
*/
@Override
public TypedValue read(EvaluationContext context, @Nullable Object target, String name) {
String value = asEnvironment(target)
.map(environment -> environment.getProperty(name))
.orElse(null);
return new TypedValue(value);
}
/**
* @inheritDoc
* @return {@literal false}.
*/
@Override
public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) {
return false;
}
/**
* @inheritDoc
*/
@Override
public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) { }
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2017-present 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.geode.function.config;
import org.apache.geode.cache.execute.Execution;
import org.apache.geode.cache.execute.Function;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.gemfire.function.config.AbstractFunctionExecutionConfigurationSource;
import org.springframework.data.gemfire.function.config.AnnotationFunctionExecutionConfigurationSource;
import org.springframework.data.gemfire.function.config.FunctionExecutionBeanDefinitionRegistrar;
import org.springframework.util.Assert;
/**
* The {@link AbstractFunctionExecutionAutoConfigurationExtension} class extends SDG's {@link FunctionExecutionBeanDefinitionRegistrar}
* to redefine the location of application POJO {@link Function} {@link Execution} interfaces.
*
* @author John Blum
* @see org.apache.geode.cache.execute.Execution
* @see org.apache.geode.cache.execute.Function
* @see org.springframework.beans.factory.BeanFactory
* @see org.springframework.beans.factory.BeanFactoryAware
* @see org.springframework.boot.autoconfigure.AutoConfigurationPackages
* @see org.springframework.core.type.AnnotationMetadata
* @see org.springframework.data.gemfire.function.config.FunctionExecutionBeanDefinitionRegistrar
* @since 1.0.0
*/
public abstract class AbstractFunctionExecutionAutoConfigurationExtension
extends FunctionExecutionBeanDefinitionRegistrar implements BeanFactoryAware {
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
protected BeanFactory getBeanFactory() {
Assert.state(this.beanFactory != null, "BeanFactory was not properly configured");
return this.beanFactory;
}
protected abstract Class<?> getConfiguration();
@SuppressWarnings("unused")
@Override
protected AbstractFunctionExecutionConfigurationSource newAnnotationBasedFunctionExecutionConfigurationSource(
AnnotationMetadata annotationMetadata) {
AnnotationMetadata metadata = AnnotationMetadata.introspect(getConfiguration());
return new AnnotationFunctionExecutionConfigurationSource(metadata) {
@Override
public Iterable<String> getBasePackages() {
return AutoConfigurationPackages.get(getBeanFactory());
}
};
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2017-present 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.geode.function.config;
import org.apache.geode.cache.execute.Execution;
import org.apache.geode.cache.execute.Function;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.data.gemfire.function.config.EnableGemfireFunctionExecutions;
/**
* The {@link GemFireFunctionExecutionAutoConfigurationRegistrar} class is a Spring {@link ImportBeanDefinitionRegistrar}
* used to register SDG POJO interfaces defining Apache Geode {@link Function} {@link Execution Executions}.
*
* @author John Blum
* @see org.apache.geode.cache.execute.Execution
* @see org.apache.geode.cache.execute.Function
* @see org.springframework.data.gemfire.function.config.EnableGemfireFunctionExecutions
* @see org.springframework.geode.function.config.AbstractFunctionExecutionAutoConfigurationExtension
* @since 1.0.0
*/
public class GemFireFunctionExecutionAutoConfigurationRegistrar
extends AbstractFunctionExecutionAutoConfigurationExtension {
@Override
protected Class<?> getConfiguration() {
return EnableGemfireFunctionExecutionsConfiguration.class;
}
@EnableGemfireFunctionExecutions
private static class EnableGemfireFunctionExecutionsConfiguration { }
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2017-present 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.geode.function.support;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.geode.cache.execute.FunctionException;
import org.apache.geode.cache.execute.ResultCollector;
/**
* The {@link AbstractResultCollector} class is an abstract base implementation of the {@link ResultCollector} interface
* encapsulating common functionality for collecting results from a Function execution.
*
* @author John Blum
* @see org.apache.geode.cache.execute.ResultCollector
* @since 1.0.0
*/
@SuppressWarnings("unused")
public abstract class AbstractResultCollector<T, S> implements ResultCollector<T, S> {
protected static final String NOT_IMPLEMENTED = "Not Implemented";
protected static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.MILLISECONDS;
private AtomicBoolean resultsEnded = new AtomicBoolean(false);
private S result = null;
@Override
public synchronized S getResult() throws FunctionException {
return this.result;
}
@Override
public S getResult(long duration, TimeUnit unit) throws FunctionException, InterruptedException {
unit = resolveTimeUnit(unit);
long durationInMilliseconds = unit.toMillis(duration);
long timeout = System.currentTimeMillis() + unit.toMillis(duration);
long waitInMilliseconds = Math.max(50, Math.min(durationInMilliseconds / 5, durationInMilliseconds));
synchronized (this) {
while (getResult() == null && System.currentTimeMillis() < timeout) {
unit.timedWait(this, waitInMilliseconds);
}
}
return getResult();
}
protected synchronized void setResult(S result) {
this.result = result;
}
protected TimeUnit resolveTimeUnit(TimeUnit unit) {
return unit != null ? unit : DEFAULT_TIME_UNIT;
}
@Override
public void clearResults() {
setResult(null);
}
@Override
public void endResults() {
this.resultsEnded.set(true);
}
protected boolean hasResultsEnded() {
return this.resultsEnded.get();
}
protected boolean hasResultsNotEnded() {
return !this.resultsEnded.get();
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2017-present 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.geode.function.support;
import java.util.Collections;
import java.util.Iterator;
import java.util.Optional;
import org.apache.geode.cache.execute.ResultCollector;
import org.apache.geode.distributed.DistributedMember;
/**
* The {@link SingleResultReturningCollector} class is an implementation of the {@link ResultCollector} interface
* which returns a single {@link Object result}.
*
* @author John Blum
* @see org.apache.geode.cache.execute.ResultCollector
* @see org.springframework.geode.function.support.AbstractResultCollector
* @since 1.0.0
*/
@SuppressWarnings("unused")
public class SingleResultReturningCollector<T> extends AbstractResultCollector<T, T> {
@Override
public void addResult(DistributedMember memberID, T resultOfSingleExecution) {
setResult(extractSingleResult(resultOfSingleExecution));
}
@SuppressWarnings("unchecked")
private <T> T extractSingleResult(Object result) {
return (T) Optional.ofNullable(result)
.filter(this::isInstanceOfIterableOrIterator)
.map(this::toIterator)
.filter(Iterator::hasNext)
.map(Iterator::next)
.map(this::extractSingleResult)
.orElseGet(() -> isInstanceOfIterableOrIterator(result) ? null : result);
}
private boolean isInstanceOfIterableOrIterator(Object obj) {
return obj instanceof Iterable || obj instanceof Iterator;
}
@SuppressWarnings("unchecked")
private <T> Iterator<T> toIterator(Object obj) {
return obj instanceof Iterator ? (Iterator<T>) obj : toIterator((Iterable<T>) obj);
}
private <T> Iterator<T> toIterator(Iterable<T> iterable) {
return iterable != null ? iterable.iterator() : Collections.emptyIterator();
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2017-present 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.geode.jackson.databind.serializer;
import java.io.IOException;
import java.math.BigDecimal;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.std.NumberSerializers;
/**
* The {@link BigDecimalSerializer} class is a {@link NumberSerializers NumberSerializers.Base} serializer
* for serializing {@link BigDecimal} values.
*
* @author John Blum
* @see java.math.BigDecimal
* @see com.fasterxml.jackson.databind.ser.std.NumberSerializers
* @since 1.3.0
*/
@SuppressWarnings("unused")
public class BigDecimalSerializer extends NumberSerializers.Base<BigDecimal> {
public static final BigDecimalSerializer INSTANCE = new BigDecimalSerializer();
public BigDecimalSerializer() {
super(BigDecimal.class, JsonParser.NumberType.BIG_DECIMAL, "bigdecimal");
}
@Override
public void serialize(BigDecimal value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
jsonGenerator.writeNumber(value);
}
@Override
public void serializeWithType(BigDecimal value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider,
TypeSerializer typeSerializer) throws IOException {
serialize(value, jsonGenerator, serializerProvider);
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2017-present 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.geode.jackson.databind.serializer;
import java.io.IOException;
import java.math.BigInteger;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.std.NumberSerializers;
/**
* The {@link BigIntegerSerializer} class is a {@link NumberSerializers NumberSerializers.Base} serializer
* for serializing {@link BigInteger} values.
*
* @author John Blum
* @see java.math.BigInteger
* @see com.fasterxml.jackson.databind.ser.std.NumberSerializers
* @since 1.3.0
*/
@SuppressWarnings("unused")
public class BigIntegerSerializer extends NumberSerializers.Base<BigInteger> {
public static final BigIntegerSerializer INSTANCE = new BigIntegerSerializer();
public BigIntegerSerializer() {
super(BigInteger.class, JsonParser.NumberType.BIG_INTEGER, "biginteger");
}
@Override
public void serialize(BigInteger value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
jsonGenerator.writeNumber(value);
}
@Override
public void serializeWithType(BigInteger value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider,
TypeSerializer typeSerializer) throws IOException {
serialize(value, jsonGenerator, serializerProvider);
}
}

View File

@@ -0,0 +1,157 @@
/*
* Copyright 2017-present 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.geode.jackson.databind.serializer;
import java.io.IOException;
import java.util.Collection;
import java.util.Objects;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.impl.PropertySerializerMap;
import com.fasterxml.jackson.databind.ser.std.AsArraySerializerBase;
import com.fasterxml.jackson.databind.ser.std.CollectionSerializer;
import org.springframework.data.gemfire.util.CollectionUtils;
/**
* The {@link TypelessCollectionSerializer} class is a custom, typeless {@link CollectionSerializer} implementation.
*
* This {@link AsArraySerializerBase} implementation is a lot like {@link CollectionSerializer}, however it excludes
* unnecessary type metadata in the context of Apache Geode.
*
* @author John Blum
* @see java.util.Collection
* @see com.fasterxml.jackson.core.JsonGenerator
* @see com.fasterxml.jackson.databind.JsonSerializer
* @see com.fasterxml.jackson.databind.ObjectMapper
* @see com.fasterxml.jackson.databind.SerializerProvider
* @see com.fasterxml.jackson.databind.ser.std.AsArraySerializerBase
* @since 1.3.0
*/
@SuppressWarnings("unused")
public class TypelessCollectionSerializer extends AsArraySerializerBase<Collection<?>> {
protected static final boolean DEFAULT_UNWRAP_SINGLE = false;
protected static final boolean DEFAULT_STATIC_TYPING = false;
public TypelessCollectionSerializer(ObjectMapper mapper) {
super(Collection.class, mapper.getTypeFactory().constructType(Object.class), DEFAULT_STATIC_TYPING,
null, null);
}
public TypelessCollectionSerializer(TypelessCollectionSerializer serializer, BeanProperty property,
TypeSerializer typeSerializer, JsonSerializer<?> elementSerializer) {
super(serializer, property, typeSerializer, elementSerializer, DEFAULT_UNWRAP_SINGLE);
}
@Override
public void serializeWithType(Collection<?> value, JsonGenerator jsonGenerator,
SerializerProvider serializerProvider, TypeSerializer typeSerializer) throws IOException {
serialize(value, jsonGenerator, serializerProvider);
}
@Override
public boolean hasSingleElement(Collection<?> value) {
return value != null && value.size() == 1;
}
@Override
protected void serializeContents(Collection<?> value, JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
jsonGenerator.setCurrentValue(value);
PropertySerializerMap serializers = this._dynamicSerializers;
TypeSerializer typeSerializer = this._valueTypeSerializer;
int index = -1;
try {
for (Object element : CollectionUtils.nullSafeCollection(value)) {
index++;
if (Objects.isNull(element)) {
serializerProvider.defaultSerializeNull(jsonGenerator);
}
else {
Class<?> elementType = element.getClass();
JsonSerializer<Object> serializer = resolveSerializer(serializerProvider, elementType);
if (typeSerializer != null) {
serializer.serializeWithType(element, jsonGenerator, serializerProvider, typeSerializer);
}
else {
serializer.serialize(element, jsonGenerator, serializerProvider);
}
}
}
}
catch(Exception cause) {
wrapAndThrow(serializerProvider, cause, value, index);
}
}
private JavaType constructSpecializedType(SerializerProvider serializerProvider, JavaType baseType, Class<?> subclass) {
return serializerProvider.constructSpecializedType(baseType, subclass);
}
private JsonSerializer<Object> resolveSerializer(SerializerProvider serializerProvider, Class<?> type)
throws JsonMappingException {
JsonSerializer<Object> resolvedSerializer = this._elementSerializer;
if (Objects.isNull(resolvedSerializer)) {
PropertySerializerMap dynamicSerializers = this._dynamicSerializers;
resolvedSerializer = dynamicSerializers.serializerFor(type);
if (Objects.isNull(resolvedSerializer)) {
resolvedSerializer = Objects.nonNull(this._elementType) && this._elementType.hasGenericTypes()
? this._findAndAddDynamic(dynamicSerializers,
constructSpecializedType(serializerProvider, this._elementType, type), serializerProvider)
: this._findAndAddDynamic(dynamicSerializers, type, serializerProvider);
}
}
return resolvedSerializer;
}
@Override
public TypelessCollectionSerializer withResolved(BeanProperty property, TypeSerializer typeSerializer,
JsonSerializer<?> elementSerializer, Boolean unwrapSingle) {
return new TypelessCollectionSerializer(this, property, typeSerializer, elementSerializer);
}
@Override
public TypelessCollectionSerializer _withValueTypeSerializer(TypeSerializer typeSerializer) {
return new TypelessCollectionSerializer(this, this._property, typeSerializer, this._elementSerializer);
}
}

View File

@@ -0,0 +1,169 @@
/*
* Copyright 2017-present 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.geode.pdx;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import org.apache.geode.cache.GemFireCache;
import org.apache.geode.pdx.PdxSerializer;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.data.gemfire.mapping.MappingPdxSerializer;
import org.springframework.data.gemfire.util.ArrayUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* Spring {@link BeanPostProcessor} used to register additional {@link Class types} handled by
* the SDG {@link MappingPdxSerializer}.
*
* @author John Blum
* @see java.lang.Class
* @see java.util.function.Predicate
* @see org.apache.geode.cache.GemFireCache
* @see org.apache.geode.pdx.PdxSerializer
* @see org.springframework.beans.factory.config.BeanPostProcessor
* @see org.springframework.data.gemfire.mapping.MappingPdxSerializer
* @since 1.5.0
*/
@SuppressWarnings("unused")
public class MappingPdxSerializerIncludedTypesRegistrar implements BeanPostProcessor {
/**
* Factory methods used to construct a new instance of {@link MappingPdxSerializerIncludedTypesRegistrar}
* initialized with given, required array of {@link Class} types that will be registered with
* SDG's {@link MappingPdxSerializer} in order to de/serialize the specified {@link Class} types as PDX.
*
* @param types array of {@link Class} types to be de/serialized as PDX using SDG's {@link MappingPdxSerializer};
* must not be {@literal null}.
* @return a new instance of {@link MappingPdxSerializerIncludedTypesRegistrar}
* @see #MappingPdxSerializerIncludedTypesRegistrar(Class[])
*/
public static @NonNull MappingPdxSerializerIncludedTypesRegistrar with(Class<?>... types) {
return new MappingPdxSerializerIncludedTypesRegistrar(types);
}
private final Class<?>[] types;
/**
* Constructs a new instance of {@link MappingPdxSerializerIncludedTypesRegistrar} initialized with given,
* required array of {@link Class} types that will be registered with SDG's {@link MappingPdxSerializer}
* in order to de/serialize the specified {@link Class} types as PDX.
*
* @param types array of {@link Class} types to be de/serialized as PDX using SDG's {@link MappingPdxSerializer};
* must not be {@literal null}.
* @see java.lang.Class
*/
public MappingPdxSerializerIncludedTypesRegistrar(@NonNull Class<?>[] types) {
this.types = Arrays.stream(ArrayUtils.nullSafeArray(types, Class.class))
.filter(Objects::nonNull)
.toArray(Class[]::new);
}
/**
* Gets the array of {@link Class} types to register with SDG's {@link MappingPdxSerializer} in order to
* de/serialize the {@link Class} types as PDX.
*
* @return the configured array of {@link Class} types to be registered with SDG's {@link MappingPdxSerializer}
* in order to de/serialize the {@link Class} types as PDX; never {@literal null}.
* @see java.lang.Class
*/
protected @NonNull Class<?>[] getTypes() {
return this.types;
}
/**
* Composes an {@link Optional} {@link Predicate} consisting of the configured array {@link Class} types
* used to match possible types de/serialized as PDX using SDG's {@link MappingPdxSerializer}.
*
* @return an {@link Optional} composite {@link Predicate} consisting of the configured
* array of {@link Class} types.
* @see java.util.function.Predicate
* @see java.util.Optional
* @see #getTypes()
*/
protected Optional<Predicate<Class<?>>> getCompositeIncludeTypeFilter() {
Predicate<Class<?>> compositeIncludeTypeFilter = null;
for (Class<?> type : getTypes()) {
if (type != null) {
compositeIncludeTypeFilter = compositeIncludeTypeFilter != null
? compositeIncludeTypeFilter.or(newIncludeTypeFilter(type))
: newIncludeTypeFilter(type);
}
}
return Optional.ofNullable(compositeIncludeTypeFilter);
}
/**
* Null-safe method used to construct a new {@link Class type} include {@link Predicate filter}
* that can be registered with SDG's {@link MappingPdxSerializer}.
*
* The {@link Predicate} matches tested {@link Class types} that are
* {@link Class#isAssignableFrom(Class) assignable from} the given {@link Class type}.
*
* @param type {@link Class} used as the basis for matching in the {@link Predicate}.
* @return an (optional} {@link Predicate} from the given {@link Class type};
* returns {@literal null} if the given {@link Class type} is {@literal null}.
* @see java.util.function.Predicate
* @see java.lang.Class
*/
protected @Nullable Predicate<Class<?>> newIncludeTypeFilter(@Nullable Class<?> type) {
return type != null
? testType -> Objects.nonNull(testType) && type.isAssignableFrom(testType)
: null;
}
/**
* Registers the configured {@link Class} types with SDG's {@link MappingPdxSerializer} providing the bean
* to post process after initialization is a {@link GemFireCache} instance and SDG's {@link MappingPdxSerializer}
* was configured as the cache's {@link PdxSerializer} used to de/serialize objects of the specified {@link Class}
* types.
*
* @param bean {@link Object bean} to evaluate.
* @param beanName {@link String} specifying the {@literal name} of the bean in the Spring container.
* @return the given {@link Object} bean.
* @throws BeansException if post processing of the bean fails.
*/
@Override
public @Nullable Object postProcessAfterInitialization(@Nullable Object bean, @Nullable String beanName)
throws BeansException {
if (bean instanceof GemFireCache) {
GemFireCache cache = (GemFireCache) bean;
PdxSerializer pdxSerializer = cache.getPdxSerializer();
if (pdxSerializer instanceof MappingPdxSerializer) {
MappingPdxSerializer mappingPdxSerializer = (MappingPdxSerializer) pdxSerializer;
getCompositeIncludeTypeFilter().ifPresent(mappingPdxSerializer::setIncludeTypeFilters);
}
}
return bean;
}
}

View File

@@ -0,0 +1,420 @@
/*
* Copyright 2017-present 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.geode.pdx;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.geode.pdx.PdxFieldDoesNotExistException;
import org.apache.geode.pdx.PdxFieldTypeMismatchException;
import org.apache.geode.pdx.PdxInstance;
import org.apache.geode.pdx.WritablePdxInstance;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessor;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.annotation.Id;
import org.springframework.data.gemfire.util.ArrayUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* A {@link PdxInstance} implementation that adapts (wraps) a non-null {@link Object} as a {@link PdxInstance}.
*
* @author John Blum
* @see java.beans.PropertyDescriptor
* @see java.lang.reflect.Field
* @see org.apache.geode.pdx.PdxInstance
* @see org.apache.geode.pdx.WritablePdxInstance
* @see org.springframework.beans.BeanWrapper
* @see org.springframework.beans.PropertyAccessor
* @see org.springframework.beans.PropertyAccessorFactory
* @since 1.3.0
*/
public class ObjectPdxInstanceAdapter implements PdxInstance {
protected static final String CLASS_PROPERTY_NAME = "class";
protected static final String ID_PROPERTY_NAME = "id";
private static void assertCondition(boolean condition, Supplier<RuntimeException> runtimeExceptionSupplier) {
if (!condition) {
throw runtimeExceptionSupplier.get();
}
}
/**
* Factory method used to construct a new instance of the {@link ObjectPdxInstanceAdapter} from
* the given {@literal target} {@link Object}.
*
* @param target {@link Object} to adapt as a {@link PdxInstance}; must not be {@literal null}.
* @return a new instance of {@link ObjectPdxInstanceAdapter}.
* @throws IllegalArgumentException if {@link Object} is {@literal null}.
* @see #ObjectPdxInstanceAdapter(Object)
*/
public static ObjectPdxInstanceAdapter from(@NonNull Object target) {
return new ObjectPdxInstanceAdapter(target);
}
/**
* Null-safe factory method used to unwrap the given {@link PdxInstance}, returning the underlying, target
* {@link PdxInstance#getObject() Object} upon which this {@link PdxInstance} is based.
*
* @param pdxInstance {@link PdxInstance} to unwrap.
* @return the underlying, target {@link PdxInstance#getObject() Object} from the given {@link PdxInstance}.
* @see org.apache.geode.pdx.PdxInstance
*/
public static @Nullable Object unwrap(@Nullable PdxInstance pdxInstance) {
return pdxInstance instanceof ObjectPdxInstanceAdapter
? pdxInstance.getObject()
: pdxInstance;
}
private final AtomicReference<String> resolvedIdentityFieldName = new AtomicReference<>(null);
private transient final BeanWrapper beanWrapper;
private final Object target;
/**
* Constructs a new instance of {@link ObjectPdxInstanceAdapter} initialized with the given {@link Object}.
*
* @param target {@link Object} to adapt as a {@link PdxInstance}; must not be {@literal null}.
* @throws IllegalArgumentException if {@link Object} is {@literal null}.
* @see java.lang.Object
*/
public ObjectPdxInstanceAdapter(Object target) {
Assert.notNull(target, "Object to adapt must not be null");
this.target = target;
this.beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(target);
}
/**
* Returns a {@link BeanWrapper} wrapping the {@literal target} {@link Object} in order to access the {@link Object}
* as a Java bean using JavaBeans conventions.
*
* @return a {@link BeanWrapper} for the {@literal target} {@link Object}; never {@literal null}.
* @see org.springframework.beans.BeanWrapper
*/
protected @NonNull BeanWrapper getBeanWrapper() {
return this.beanWrapper;
}
/**
* Returns the {@link Class#getName()} of the underlying, target {@link Object}.
*
* @return the {@link Class#getName()} of the underlying, target {@link Object}.
* @see java.lang.Object#getClass()
* @see java.lang.Class#getName()
* @see #getObject()
*/
@Override
public String getClassName() {
return getObject().getClass().getName();
}
/**
* Determines whether this {@link PdxInstance} can be deserialized back into an {@link Object}.
*
* This method effectively returns {@literal true} since this {@link PdxInstance} implementation is an adapter
* for an underlying, target {@link Object} in the first place.
*
* @return a boolean value indicating whether this {@link PdxInstance} can be deserialized
* back into an {@link Object}.
* @see #getObject()
*/
@Override
public boolean isDeserializable() {
return getObject() != null;
}
/**
* Determines whether the underlying, target {@link Object} is an {@link Enum enumerated value} {@link Class type}.
*
* @return a boolean value indicating whether the underlying, target {@link Object}
* is an {@link Enum enumerated value} {@link Class type}.
* @see java.lang.Object#getClass()
* @see java.lang.Class#isEnum()
* @see #getObject()
*/
@Override
public boolean isEnum() {
return getObject().getClass().isEnum();
}
/**
* Returns the {@link Object value} for the {@link PropertyDescriptor property} identified by
* the given {@link String field name} on the underlying, target {@link Object}.
*
* @param fieldName {@link String} containing the name of the field to get the {@link Object value} for.
* @return the {@link Object value} for the {@link PropertyDescriptor property} identified by
* the given {@link String field name} on the underlying, target {@link Object}.
* @see org.springframework.beans.BeanWrapper#getPropertyValue(String)
* @see #getBeanWrapper()
*/
@Override
public Object getField(String fieldName) {
BeanWrapper beanWrapper = getBeanWrapper();
return beanWrapper.isReadableProperty(fieldName)
? beanWrapper.getPropertyValue(fieldName)
: null;
}
/**
* Returns a {@link List} of {@link String field names} based on the {@link PropertyDescriptor propeties}
* from the underlying, target {@link Object}.
*
* @return a {@link List} of {@link String field names} / {@link PropertyDescriptor properties} serialized
* in the PDX bytes for the underlying, target {@link Object}.
* @see org.springframework.beans.BeanWrapper#getPropertyDescriptors()
* @see java.beans.PropertyDescriptor
* @see #getBeanWrapper()
*/
@Override
public List<String> getFieldNames() {
PropertyDescriptor[] propertyDescriptors =
ArrayUtils.nullSafeArray(getBeanWrapper().getPropertyDescriptors(), PropertyDescriptor.class);
return Arrays.stream(propertyDescriptors)
.map(PropertyDescriptor::getName)
.filter(propertyName -> !CLASS_PROPERTY_NAME.equals(propertyName))
.collect(Collectors.toList());
}
/**
* Determines whether the given {@link String field name} is an identifier for this {@link PdxInstance}.
*
* @param fieldName {@link String} containing the name of the field to evaluate.
* @return a boolean value indicating whether the given {@link String field name} is an identifier for
* this {@link PdxInstance}.
* @see #resolveIdentityFieldNameFromProperty(BeanWrapper)
*/
@Override
public boolean isIdentityField(String fieldName) {
String resolvedIdentityFieldName = this.resolvedIdentityFieldName.updateAndGet(it ->
StringUtils.hasText(it) ? it : resolveIdentityFieldNameFromProperty());
return StringUtils.hasText(resolvedIdentityFieldName) && resolvedIdentityFieldName.equals(fieldName);
}
// Identifier Search Algorithm: @Id Property -> @Id Field -> "id" Property
@Nullable String resolveIdentityFieldNameFromProperty() {
return resolveIdentityFieldNameFromProperty(getBeanWrapper());
}
private @Nullable String resolveIdentityFieldNameFromProperty(@NonNull BeanWrapper beanWrapper) {
List<PropertyDescriptor> properties =
Arrays.asList(ArrayUtils.nullSafeArray(beanWrapper.getPropertyDescriptors(), PropertyDescriptor.class));
Optional<PropertyDescriptor> atIdAnnotatedProperty = properties.stream()
.filter(this::isAtIdAnnotatedProperty)
.findFirst();
return atIdAnnotatedProperty
.map(PropertyDescriptor::getName)
.orElseGet(() -> resolveIdentityFieldNameFromField(beanWrapper));
}
private boolean isAtIdAnnotatedProperty(@Nullable PropertyDescriptor propertyDescriptor) {
return Optional.ofNullable(propertyDescriptor)
.map(PropertyDescriptor::getReadMethod)
.map(method -> AnnotationUtils.findAnnotation(method, Id.class))
.isPresent();
}
private @Nullable String resolveIdentityFieldNameFromField(@NonNull BeanWrapper beanWrapper) {
List<Field> fields =
Arrays.asList(ArrayUtils.nullSafeArray(beanWrapper.getWrappedClass().getDeclaredFields(), Field.class));
Optional<PropertyDescriptor> atIdAnnotatedProperty = fields.stream()
.map(field -> getPropertyForAtIdAnnotatedField(beanWrapper, field))
.filter(Objects::nonNull)
.findFirst();
return atIdAnnotatedProperty
.map(PropertyDescriptor::getName)
.orElseGet(() -> beanWrapper.isReadableProperty(ID_PROPERTY_NAME)
? ID_PROPERTY_NAME
: null);
}
private @Nullable PropertyDescriptor getPropertyForAtIdAnnotatedField(@NonNull BeanWrapper beanWrapper,
@Nullable Field field) {
return Optional.ofNullable(field)
.filter(it -> beanWrapper.isReadableProperty(it.getName()))
.filter(it -> Objects.nonNull(AnnotationUtils.findAnnotation(it, Id.class)))
.map(it -> beanWrapper.getPropertyDescriptor(it.getName()))
.orElse(null);
}
/**
* Returns the {@literal target} {@link Object} being adapted by this {@link PdxInstance}.
*
* @return the {@literal target} {@link Object} being adapted by this {@link PdxInstance}; never {@literal null}.
* @see java.lang.Object
*/
@Override
public Object getObject() {
return this.target;
}
ObjectPdxInstanceAdapter getParent() {
return this;
}
/**
* @inheritDoc
*/
@Override
public WritablePdxInstance createWriter() {
return new WritablePdxInstance() {
@Override
public void setField(String fieldName, Object value) {
withPropertyAccessorFor(fieldName, value).setPropertyValue(fieldName, value);
}
private PropertyAccessor withPropertyAccessorFor(String fieldName, Object value) {
assertFieldIsPresent(fieldName);
BeanWrapper beanWrapper = getBeanWrapper();
assertFieldIsWritable(beanWrapper, fieldName);
assertValueIsTypeMatch(beanWrapper, fieldName, value);
return beanWrapper;
}
private void assertFieldIsPresent(String fieldName) {
Supplier<String> pdxFieldNotFoundExceptionMessageSupplier = () ->
String.format("Field [%1$s] does not exist on Object [%2$s]", fieldName, getClassName());
assertCondition(hasField(fieldName),
() -> new PdxFieldDoesNotExistException(pdxFieldNotFoundExceptionMessageSupplier.get()));
}
private void assertFieldIsWritable(BeanWrapper beanWrapper, String fieldName) {
Supplier<String> pdxFieldNotWritableExceptionMessageSupplier = () ->
String.format("Field [%1$s] of Object [%2$s] is not writable", fieldName, getClassName());
assertCondition(beanWrapper.isWritableProperty(fieldName),
() -> new PdxFieldNotWritableException(pdxFieldNotWritableExceptionMessageSupplier.get()));
}
private void assertValueIsTypeMatch(BeanWrapper beanWrapper, String fieldName, Object value) {
PropertyDescriptor property = beanWrapper.getPropertyDescriptor(fieldName);
Supplier<String> typeMismatchExceptionMessageSupplier = () ->
String.format("Value [%1$s] of type [%2$s] does not match field [%3$s] of type [%4$s] on Object [%5$s]",
value, ObjectUtils.nullSafeClassName(value), fieldName, property.getPropertyType().getName(), getClassName());
assertCondition(isTypeMatch(property, value),
() -> new PdxFieldTypeMismatchException(typeMismatchExceptionMessageSupplier.get()));
}
private boolean isTypeMatch(PropertyDescriptor property, Object value) {
return value == null || property.getPropertyType().isInstance(value);
}
@Override
public String getClassName() {
return getParent().getClassName();
}
@Override
public boolean isDeserializable() {
return getParent().isDeserializable();
}
@Override
public boolean isEnum() {
return getParent().isEnum();
}
@Override
public Object getField(String fieldName) {
return getParent().getField(fieldName);
}
@Override
public List<String> getFieldNames() {
return getParent().getFieldNames();
}
@Override
public boolean isIdentityField(String fieldName) {
return getParent().isIdentityField(fieldName);
}
@Override
public Object getObject() {
return getParent().getObject();
}
@Override
public WritablePdxInstance createWriter() {
return this;
}
@Override
public boolean hasField(String fieldName) {
return getParent().hasField(fieldName);
}
};
}
/**
* Determines whether the given {@link String field name} is a {@link PropertyDescriptor property}
* on the underlying, target {@link Object}.
*
* @param fieldName {@link String} containing the name of the field to match against
* a {@link PropertyDescriptor property} from the underlying, target {@link Object}.
* @return a boolean value that determines whether the given {@link String field name}
* is a {@link PropertyDescriptor property} on the underlying, target {@link Object}.
* @see #getFieldNames()
*/
@Override
public boolean hasField(String fieldName) {
return getFieldNames().contains(fieldName);
}
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright 2017-present 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.geode.security.support;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.geode.security.AuthenticationFailedException;
import org.apache.geode.security.ResourcePermission;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.gemfire.support.LazyWiringDeclarableSupport;
import org.springframework.util.Assert;
/**
* The {@link SecurityManagerProxy} class is an Apache Geode {@link org.apache.geode.security.SecurityManager}
* proxy implementation delegating to a backing {@link org.apache.geode.security.SecurityManager} implementation
* which is registered as a managed bean in a Spring context.
*
* The idea behind this {@link org.apache.geode.security.SecurityManager} is to enable users to be able to configure
* and manage the {@code SecurityManager} as a Spring bean. However, Apache Geode require
* the {@link org.apache.geode.security.SecurityManager} to be configured using a System property when launching
* Apache Geode Servers with Gfsh, which makes it difficult to "manage" the {@code SecurityManager} instance.
*
* Therefore, this implementation allows a developer to set the Apache Geode System property using this proxy...
*
* <code>
* gemfire.security-manager=org.springframework.geode.security.support.SecurityManagerProxy
* </code>
*
* And then declare and define a bean in the Spring context implementing the
* {@link org.apache.geode.security.SecurityManager} interface...
*
* <code>
* Configuration
* class MyApplicationConfiguration {
*
* Bean
* ExampleSecurityManager exampleSecurityManager(Environment environment) {
* return new ExampleSecurityManager(environment);
* }
*
* ...
* }
* </code>
*
* @author John Blum
* @see org.apache.geode.security.ResourcePermission
* @see org.apache.geode.security.SecurityManager
* @see org.springframework.beans.factory.BeanFactory
* @see org.springframework.beans.factory.BeanFactoryAware
* @see org.springframework.beans.factory.DisposableBean
* @see org.springframework.beans.factory.annotation.Autowired
* @see org.springframework.data.gemfire.support.LazyWiringDeclarableSupport
* @since 1.0.0
*/
@SuppressWarnings("unused")
public class SecurityManagerProxy extends LazyWiringDeclarableSupport
implements org.apache.geode.security.SecurityManager, DisposableBean, BeanFactoryAware {
private static final AtomicReference<SecurityManagerProxy> INSTANCE = new AtomicReference<>();
private BeanFactory beanFactory;
private org.apache.geode.security.SecurityManager securityManager;
/**
* Returns a reference to the single {@link SecurityManagerProxy} instance configured by Apache Geode in startup.
*
* @return a reference to the single {@link SecurityManagerProxy} instance.
*/
public static SecurityManagerProxy getInstance() {
SecurityManagerProxy securityManagerProxy = INSTANCE.get();
Assert.state(securityManagerProxy != null, "SecurityManagerProxy was not configured");
return securityManagerProxy;
}
/**
* Constructs a new instance of {@link SecurityManagerProxy}, which will delegate all Apache Geode
* security operations to a Spring managed {@link org.apache.geode.security.SecurityManager} bean.
*/
public SecurityManagerProxy() {
INSTANCE.compareAndSet(null, this);
}
/**
* Configures a reference to the current Spring {@link BeanFactory}.
*
* @param beanFactory reference to the current Spring {@link BeanFactory}.
* @throws BeansException if this operation fails to configure the reference to the {@link BeanFactory}.
* @see org.springframework.beans.factory.BeanFactory
*/
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
/**
* Configures a reference to the Apache Geode {@link org.apache.geode.security.SecurityManager} instance
* delegated to by this {@link SecurityManagerProxy}.
*
* @param securityManager reference to the underlying Apache Geode {@link org.apache.geode.security.SecurityManager}
* instance delegated to by this {@link SecurityManagerProxy}.
* @throws IllegalArgumentException if the {@link org.apache.geode.security.SecurityManager} reference
* is {@literal null}.
* @see org.apache.geode.security.SecurityManager
*/
@Autowired
public void setSecurityManager(org.apache.geode.security.SecurityManager securityManager) {
Assert.notNull(securityManager, "SecurityManager must not be null");
this.securityManager = securityManager;
}
/**
* Returns a reference to the Apache Geode {@link org.apache.geode.security.SecurityManager} instance
* delegated to by this {@link SecurityManagerProxy}.
*
* @return a reference to the underlying {@link org.apache.geode.security.SecurityManager} instance
* delegated to by this {@link SecurityManagerProxy}.
* @throws IllegalStateException if the configured {@link org.apache.geode.security.SecurityManager}
* was not properly configured.
* @see org.apache.geode.security.SecurityManager
*/
protected org.apache.geode.security.SecurityManager getSecurityManager() {
Assert.state(this.securityManager != null, "No SecurityManager configured");
return this.securityManager;
}
@Override
public Object authenticate(Properties properties) throws AuthenticationFailedException {
return getSecurityManager().authenticate(properties);
}
@Override
public boolean authorize(Object principal, ResourcePermission permission) {
return getSecurityManager().authorize(principal, permission);
}
@Override
public void close() {
getSecurityManager().close();
}
@Override
public void destroy() throws Exception {
super.destroy();
INSTANCE.set(null);
}
@Override
protected BeanFactory locateBeanFactory() {
return this.beanFactory != null ? this.beanFactory : super.locateBeanFactory();
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2017-present 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.geode.security.support;
import java.util.Properties;
import org.apache.geode.security.AuthenticationFailedException;
import org.apache.geode.security.ResourcePermission;
/**
* {@link SecurityManagerSupport} is an abstract base class implementing Apache Geode's
* {@link org.apache.geode.security.SecurityManager} interface, providing default implementations of the
* {@link org.apache.geode.security.SecurityManager} auth methods.
*
* @author John Blum
* @see org.apache.geode.security.SecurityManager
* @since 1.0.0
*/
@SuppressWarnings("unused")
public abstract class SecurityManagerSupport implements org.apache.geode.security.SecurityManager {
protected static final boolean DEFAULT_AUTHORIZATION = false;
@Override
public void init(Properties securityProperties) { }
@Override
public Object authenticate(Properties credentials) throws AuthenticationFailedException {
return new AuthenticationFailedException("Authentication Provider Not Present");
}
@Override
public boolean authorize(Object principal, ResourcePermission permission) {
return DEFAULT_AUTHORIZATION;
}
@Override
public void close() { }
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2017-present 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.geode.test.context;
import java.util.Optional;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigRegistry;
import org.springframework.data.gemfire.util.ArrayUtils;
import org.springframework.geode.context.annotation.RefreshableAnnotationConfigApplicationContext;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractContextLoader;
/**
* An {@link AbstractContextLoader} from the Spring {@link TestContext} Framework used to load
* a {@link RefreshableAnnotationConfigApplicationContext}.
*
* @author John Blum
* @see org.springframework.context.ApplicationContext
* @see org.springframework.context.ConfigurableApplicationContext
* @see org.springframework.context.annotation.AnnotationConfigRegistry
* @see org.springframework.geode.context.annotation.RefreshableAnnotationConfigApplicationContext
* @see org.springframework.test.context.ContextConfigurationAttributes
* @see org.springframework.test.context.MergedContextConfiguration
* @see org.springframework.test.context.TestContext
* @see org.springframework.test.context.support.AbstractContextLoader
* @since 1.3.0
*/
@SuppressWarnings("unused")
public class TestRefreshableApplicationContextLoader extends AbstractContextLoader {
protected static final String DEFAULT_RESOURCE_SUFFIX = "-context";
private Class<?> testClass;
/**
* @inheritDoc
*/
@Override
public void processContextConfiguration(ContextConfigurationAttributes configAttributes) {
super.processContextConfiguration(configAttributes);
this.testClass = configAttributes.getDeclaringClass();
}
/**
* @inheritDoc
*/
@Override
public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) {
ConfigurableApplicationContext applicationContext =
configure(newApplicationContext(mergedConfig), mergedConfig);
applicationContext.registerShutdownHook();
applicationContext.refresh();
return applicationContext;
}
/**
* Constructs a new instance of the {@link RefreshableAnnotationConfigApplicationContext} initialized from
* the given {{@link TestContext} @link MergedContextConfiguration merged configuration meta-data}.
*
* @param contextConfiguration {@link MergedContextConfiguration} from the {@link TestContext} used to configure
* and bootstrap a new {@link RefreshableAnnotationConfigApplicationContext}.
* @return a configured and bootstrapped {@link ConfigurableApplicationContext} implementation.
* @see org.springframework.context.ConfigurableApplicationContext
* @see org.springframework.geode.context.annotation.RefreshableAnnotationConfigApplicationContext
* @see org.springframework.test.context.MergedContextConfiguration
* @see #prepareContext(ConfigurableApplicationContext, MergedContextConfiguration)
*/
protected ConfigurableApplicationContext newApplicationContext(MergedContextConfiguration contextConfiguration) {
RefreshableAnnotationConfigApplicationContext applicationContext =
new RefreshableAnnotationConfigApplicationContext(contextConfiguration.getParentApplicationContext());
prepareContext(applicationContext, contextConfiguration);
return applicationContext;
}
private ConfigurableApplicationContext configure(ConfigurableApplicationContext applicationContext,
MergedContextConfiguration contextConfiguration) {
applicationContext = configureComponentClasses(applicationContext, contextConfiguration);
applicationContext = configureScan(applicationContext, contextConfiguration);
customizeContext(applicationContext, contextConfiguration);
return applicationContext;
}
private ConfigurableApplicationContext configureComponentClasses(
ConfigurableApplicationContext applicationContext, MergedContextConfiguration contextConfiguration) {
Optional.ofNullable(applicationContext)
.filter(it -> ArrayUtils.isNotEmpty(contextConfiguration.getClasses()))
.filter(AnnotationConfigRegistry.class::isInstance)
.map(AnnotationConfigRegistry.class::cast)
.ifPresent(registry -> registry.register(contextConfiguration.getClasses()));
return applicationContext;
}
private ConfigurableApplicationContext configureScan(ConfigurableApplicationContext applicationContext,
MergedContextConfiguration contextConfiguration) {
Optional.ofNullable(applicationContext)
.filter(it -> ArrayUtils.isNotEmpty(contextConfiguration.getLocations()))
.filter(AnnotationConfigRegistry.class::isInstance)
.map(AnnotationConfigRegistry.class::cast)
.ifPresent(registry -> registry.scan(contextConfiguration.getLocations()));
return applicationContext;
}
/**
* @inheritDoc
*/
@Override
public ApplicationContext loadContext(String... locations) {
return loadContext(new MergedContextConfiguration(this.testClass, locations, new Class[0], new String[0],
this));
}
/**
* @inheritDoc
*/
@Override
protected String getResourceSuffix() {
return DEFAULT_RESOURCE_SUFFIX;
}
}

View File

@@ -0,0 +1,4 @@
# Spring Boot for Apache Geode Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.geode.context.logging.GeodeLoggingApplicationListener,\
org.springframework.geode.context.logging.EnvironmentLoggingApplicationListener

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2017-present 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 example.app.crm.config;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newRuntimeException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.data.cassandra.config.AbstractCassandraConfiguration;
import org.springframework.data.cassandra.config.CqlSessionFactoryBean;
import org.springframework.data.gemfire.tests.util.IOUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* Base test configuration used to configure and bootstrap an Apache Cassandra database with a schema and data.
*
* @author John Blum
* @see org.springframework.core.io.Resource
* @see org.springframework.data.cassandra.config.AbstractCassandraConfiguration
* @see org.springframework.data.cassandra.config.CqlSessionFactoryBean
* @since 1.1.0
*/
public abstract class TestCassandraConfiguration extends AbstractCassandraConfiguration {
protected static final int CASSANDRA_DEFAULT_PORT = CqlSessionFactoryBean.DEFAULT_PORT;
private static final String CASSANDRA_DATA_CQL = "cassandra-data.cql";
private static final String CASSANDRA_SCHEMA_CQL = "cassandra-schema.cql";
private static final String LOCAL_DATA_CENTER = "datacenter1";
private static final String KEYSPACE_NAME = "CustomerService";
private static final String SESSION_NAME = "CustomerServiceCluster";
@NonNull
@Override
protected String getKeyspaceName() {
return KEYSPACE_NAME;
}
@Override
protected String getLocalDataCenter() {
return LOCAL_DATA_CENTER;
}
@Nullable
@Override
protected String getSessionName() {
return SESSION_NAME;
}
/*
@Nullable @Override
protected KeyspacePopulator keyspacePopulator() {
return cqlSession -> loadCassandraCqlScripts().forEach(cqlSession::execute);
}
*/
// TODO: Remove use of deprecation after Spring Data for Apache Cassandra issues are resolved!
@Override
protected List<String> getStartupScripts() {
List<String> startupScripts = new ArrayList<>(super.getStartupScripts());
startupScripts.addAll(readLines(new ClassPathResource(CASSANDRA_SCHEMA_CQL)));
startupScripts.addAll(readLines(new ClassPathResource(CASSANDRA_DATA_CQL)));
return startupScripts;
}
private @NonNull List<String> readLines(@NonNull Resource resource) {
BufferedReader resourceReader = null;
try {
resourceReader = new BufferedReader(new InputStreamReader(resource.getInputStream()));
return resourceReader.lines()
.filter(StringUtils::hasText)
.collect(Collectors.toList());
}
catch (IOException cause) {
throw newRuntimeException(cause, "Failed to read from Resource [%s]", resource);
}
finally {
IOUtils.close(resourceReader);
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2017-present 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 example.app.crm.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.testcontainers.containers.GenericContainer;
/**
* Spring {@link @Configuration} for Apache Cassandra using Testcontainers.
*
* @author John Blum
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.Profile
* @see org.testcontainers.containers.GenericContainer
* @since 1.1.0
*/
@Configuration
@Profile("inline-caching-cassandra")
@SuppressWarnings("unused")
public class TestcontainersCassandraConfiguration extends TestCassandraConfiguration {
private static final String CASSANDRA_DOCKER_IMAGE_NAME = "cassandra:latest";
@Bean
@SuppressWarnings("rawtypes")
GenericContainer cassandraContainer() {
GenericContainer cassandraContainer = newCustomCassandraContainer();
cassandraContainer.start();
return cassandraContainer;
}
@SuppressWarnings("rawtypes")
private GenericContainer newCassandraContainer() {
return new GenericContainer(CASSANDRA_DOCKER_IMAGE_NAME)
.withExposedPorts(CASSANDRA_DEFAULT_PORT);
}
@SuppressWarnings("rawtypes")
private GenericContainer newCustomCassandraContainer() {
return newCassandraContainer()
.withEnv("HEAP_NEWSIZE", "128M")
.withEnv("MAX_HEAP_SIZE", "1024M")
.withEnv("JVM_OPTS", "-Dcassandra.skip_wait_for_gossip_to_settle=0 -Dcassandra.initial_token=0")
.withEnv("CASSANDRA_SNITCH", "GossipingPropertyFileSnitch");
}
@Override
protected String getContactPoints() {
return cassandraContainer().getContainerIpAddress();
}
@Override
protected int getPort() {
return cassandraContainer().getFirstMappedPort();
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2017-present 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 example.app.crm.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import org.springframework.data.annotation.Id;
import org.springframework.data.cassandra.core.mapping.Indexed;
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.gemfire.mapping.annotation.Region;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* The {@link Customer} class is an Abstract Data Type (ADT) modeling a customer.
*
* @author John Blum
* @see lombok
* @see org.springframework.data.annotation.Id
* @see org.springframework.data.gemfire.mapping.annotation.Region
* @since 1.1.0
*/
@Data
@Entity
@Region("Customers")
@Table(name = "Customers")
@org.springframework.data.cassandra.core.mapping.Table("Customers")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(staticName = "newCustomer")
public class Customer {
@Id
@jakarta.persistence.Id
@PrimaryKey
private Long id;
@Indexed
private String name;
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2017-present 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 example.app.crm.repo;
import org.springframework.data.repository.CrudRepository;
import example.app.crm.model.Customer;
/**
* {@link CustomerRepository} is a Spring Data {@link CrudRepository} and Data Access Object (DAO) defining basic CRUD
* and simple query data access operations on {@link Customer} objects.
*
* @author John Blum
* @see java.lang.Long
* @see org.springframework.data.repository.CrudRepository
* @see example.app.crm.model.Customer
* @since 1.1.0
*/
public interface CustomerRepository extends CrudRepository<Customer, Long> {
Customer findByName(String name);
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2017-present 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 example.app.crm.service;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import example.app.crm.model.Customer;
/**
* {@link CustomerService} is a Spring {@link Service @Service} class servicing {@link Customer Customers}.
*
* @author John Blum
* @see org.springframework.cache.annotation.Cacheable
* @see org.springframework.stereotype.Service
* @see example.app.crm.model.Customer
* @since 1.2.0
*/
@Service
public class CustomerService {
@Cacheable("CustomersByName")
public Customer findByName(String name) {
return Customer.newCustomer(System.currentTimeMillis(), name);
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2017-present 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 example.app.env;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
/**
* {@link SpringBootApplication} allowing users to review Spring's property resolution precedence.
*
* @author John Blum
* @see org.springframework.boot.ApplicationRunner
* @see org.springframework.boot.SpringApplication
* @see org.springframework.boot.autoconfigure.SpringBootApplication
* @see org.springframework.context.annotation.Bean
* @see org.springframework.core.env.Environment
* @since 1.4.0
*/
@SuppressWarnings("unused")
@SpringBootApplication(exclude = CassandraDataAutoConfiguration.class)
public class EnvironmentUsingSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(EnvironmentUsingSpringBootApplication.class, args);
}
@Value("${example.app.property:FROM-CODE}")
private String testProperty;
@Bean
ApplicationRunner environmentRunner(Environment environment) {
return args -> {
//System.err.printf("PROPERTY is [%s]%n", environment.getProperty("example.app.property"));
System.err.printf("PROPERTY is [%s]%n", this.testProperty);
};
}
}

Some files were not shown because too many files have changed in this diff Show More