├── .gitignore ├── src ├── main │ └── java │ │ └── com │ │ └── github │ │ └── msemys │ │ └── esjc │ │ ├── event │ │ ├── Event.java │ │ ├── ConnectionClosed.java │ │ ├── ClientDisconnected.java │ │ ├── AuthenticationFailed.java │ │ ├── ClientReconnecting.java │ │ ├── ErrorOccurred.java │ │ ├── ClientConnected.java │ │ ├── Events.java │ │ └── EventQueue.java │ │ ├── ssl │ │ └── SslValidationMode.java │ │ ├── util │ │ ├── EmptyArrays.java │ │ ├── Throwables.java │ │ ├── Threads.java │ │ ├── Ranges.java │ │ ├── Numbers.java │ │ ├── UUIDConverter.java │ │ ├── Iterables.java │ │ ├── Strings.java │ │ ├── concurrent │ │ │ ├── DefaultThreadFactory.java │ │ │ └── ResettableLatch.java │ │ ├── SystemTime.java │ │ ├── Preconditions.java │ │ ├── Subscriptions.java │ │ └── IntRange.java │ │ ├── task │ │ ├── Task.java │ │ ├── EstablishTcpConnection.java │ │ ├── CloseConnection.java │ │ ├── StartOperation.java │ │ ├── StartConnection.java │ │ ├── StartSubscription.java │ │ ├── StartPersistentSubscription.java │ │ └── TaskQueue.java │ │ ├── operation │ │ ├── InspectionDecision.java │ │ ├── NoResultException.java │ │ ├── InvalidTransactionException.java │ │ ├── Operation.java │ │ ├── ServerErrorException.java │ │ ├── AccessDeniedException.java │ │ ├── manager │ │ │ ├── OperationTimeoutException.java │ │ │ ├── RetriesLimitReachedException.java │ │ │ └── OperationItem.java │ │ ├── NotAuthenticatedException.java │ │ ├── StreamDeletedException.java │ │ ├── CommandNotExpectedException.java │ │ ├── StreamNotFoundException.java │ │ ├── WrongExpectedVersionException.java │ │ └── InspectionResult.java │ │ ├── VolatileSubscriptionListener.java │ │ ├── user │ │ ├── dto │ │ │ ├── UserHolder.java │ │ │ ├── ResetPasswordDetails.java │ │ │ ├── UsersHolder.java │ │ │ ├── UserUpdateDetails.java │ │ │ ├── ChangePasswordDetails.java │ │ │ └── UserCreateDetails.java │ │ ├── RelLink.java │ │ ├── UserConflictException.java │ │ ├── UserNotFoundException.java │ │ ├── UserException.java │ │ └── User.java │ │ ├── PersistentSubscriptionListener.java │ │ ├── node │ │ ├── EndpointDiscoverer.java │ │ ├── cluster │ │ │ ├── VNodeState.java │ │ │ ├── ClusterInfoDto.java │ │ │ ├── NodePreference.java │ │ │ ├── ClusterException.java │ │ │ ├── GossipSeed.java │ │ │ └── MemberInfoDto.java │ │ ├── EndpointDiscovererFactory.java │ │ ├── DelegatedEndpointDiscovererFactory.java │ │ ├── NodeEndpoints.java │ │ ├── DefaultEndpointDiscovererFactory.java │ │ └── single │ │ │ ├── SingleNodeSettings.java │ │ │ └── SingleEndpointDiscoverer.java │ │ ├── ReadDirection.java │ │ ├── projection │ │ ├── Projections.java │ │ ├── ProjectionMode.java │ │ ├── ProjectionConflictException.java │ │ ├── ProjectionNotFoundException.java │ │ ├── ProjectionException.java │ │ └── UpdateOptions.java │ │ ├── tcp │ │ ├── handler │ │ │ ├── AuthenticationEvent.java │ │ │ └── HeartbeatHandler.java │ │ ├── TcpPackageEncoder.java │ │ ├── TcpFlag.java │ │ ├── TcpPackageDecoder.java │ │ └── TcpCommand.java │ │ ├── PersistentSubscriptionDeleteStatus.java │ │ ├── StreamPosition.java │ │ ├── SliceReadStatus.java │ │ ├── subscription │ │ ├── PersistentSubscriptionProtocol.java │ │ ├── MaximumSubscribersReachedException.java │ │ ├── PersistentSubscriptionDeletedException.java │ │ ├── SubscriptionBufferOverflowException.java │ │ ├── PersistentSubscriptionNakEventAction.java │ │ ├── VolatileSubscription.java │ │ ├── SubscriptionOperation.java │ │ ├── PersistentSubscriptionChannel.java │ │ ├── manager │ │ │ └── SubscriptionItem.java │ │ └── VolatileSubscriptionOperation.java │ │ ├── EventStoreListener.java │ │ ├── WriteStatus.java │ │ ├── PersistentSubscriptionCreateStatus.java │ │ ├── PersistentSubscriptionCreateResult.java │ │ ├── PersistentSubscriptionDeleteResult.java │ │ ├── PersistentSubscriptionUpdateResult.java │ │ ├── EventReadStatus.java │ │ ├── system │ │ ├── SystemEventTypes.java │ │ ├── SystemStreams.java │ │ ├── SystemConsumerStrategy.java │ │ └── SystemProjections.java │ │ ├── DeleteResult.java │ │ ├── CatchUpSubscriptionListener.java │ │ ├── http │ │ ├── HttpOperationTimeoutException.java │ │ ├── HttpClientException.java │ │ └── handler │ │ │ └── HttpResponseHandler.java │ │ ├── PersistentSubscriptionUpdateStatus.java │ │ ├── transaction │ │ └── TransactionManager.java │ │ ├── ConnectionClosedException.java │ │ ├── RetryableResolvedEvent.java │ │ ├── CannotEstablishConnectionException.java │ │ ├── WriteResult.java │ │ ├── AllEventsIterator.java │ │ ├── WriteAttemptResult.java │ │ ├── AllEventsSpliterator.java │ │ ├── SubscriptionListener.java │ │ ├── UserCredentials.java │ │ ├── EventStoreException.java │ │ ├── EventReadResult.java │ │ ├── StreamMetadataResult.java │ │ ├── RawStreamMetadataResult.java │ │ ├── Subscription.java │ │ ├── StreamEventsIterator.java │ │ ├── StreamEventsSpliterator.java │ │ ├── AbstractEventsIterator.java │ │ ├── ExpectedVersion.java │ │ ├── SubscriptionDropReason.java │ │ ├── AllEventsSlice.java │ │ ├── SystemSettingsJsonAdapter.java │ │ ├── RecordedEvent.java │ │ ├── StreamEventsSlice.java │ │ ├── ResolvedEvent.java │ │ ├── AbstractEventsSpliterator.java │ │ ├── StreamAclJsonAdapter.java │ │ └── Position.java └── test │ ├── java │ └── com │ │ └── github │ │ └── msemys │ │ └── esjc │ │ ├── rule │ │ ├── Retryable.java │ │ └── RetryableMethodRule.java │ │ ├── runner │ │ ├── EventStoreRunnerWithParametersFactory.java │ │ └── EventStoreRunnerWithParameters.java │ │ ├── ITWhenWorkingWithMetadata.java │ │ ├── ITReadEventOfLinkToToDeletedEvent.java │ │ ├── matcher │ │ ├── IteratorSizeMatcher.java │ │ ├── RecordedEventMatcher.java │ │ └── RecordedEventListMatcher.java │ │ ├── ITReadAllEventsBackwardWithLinkToDeletedEvents.java │ │ ├── ITReadAllEventsForwardWithLinkToToDeletedEvents.java │ │ ├── ITSubscribeToAll.java │ │ ├── ITReadStreamEventsForwardWithUnresolvedLinkTo.java │ │ ├── ITEventStoreListener.java │ │ ├── ITTryAppendToStream.java │ │ ├── ITReadAllEventsForwardWithLinkToPassedMaxCount.java │ │ ├── ITReadAllEventsForwardWithHardDeletedStream.java │ │ ├── AbstractSslConnectionTest.java │ │ ├── ITSslCommonNameConnection.java │ │ ├── ITReadAllEventsForwardWithSoftDeletedStream.java │ │ ├── ITJsonFlagOnEventData.java │ │ ├── ITSubscribeToStream.java │ │ ├── ITDeleteStream.java │ │ └── ITSslCertificateConnection.java │ └── resources │ └── logback.xml ├── .travis.yml ├── docker-compose.yml ├── scripts └── generate-ssl-cert.sh └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.iml 3 | .idea 4 | *.*~ 5 | ssl 6 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/event/Event.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.event; 2 | 3 | /** 4 | * Client event. 5 | */ 6 | public interface Event { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/ssl/SslValidationMode.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.ssl; 2 | 3 | public enum SslValidationMode { 4 | COMMON_NAME, 5 | CERTIFICATE, 6 | NONE 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/util/EmptyArrays.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.util; 2 | 3 | public class EmptyArrays { 4 | 5 | public static final byte[] EMPTY_BYTES = new byte[0]; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/task/Task.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.task; 2 | 3 | public interface Task { 4 | 5 | default void fail(Exception exception) { 6 | // do nothing 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/event/ConnectionClosed.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.event; 2 | 3 | /** 4 | * Fired when a client connection is closed. 5 | */ 6 | public class ConnectionClosed implements Event { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/event/ClientDisconnected.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.event; 2 | 3 | /** 4 | * Fired when a client is disconnected from an Event Store server. 5 | */ 6 | public class ClientDisconnected implements Event { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/operation/InspectionDecision.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.operation; 2 | 3 | public enum InspectionDecision { 4 | DoNothing, 5 | EndOperation, 6 | Retry, 7 | Reconnect, 8 | Subscribed 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/event/AuthenticationFailed.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.event; 2 | 3 | /** 4 | * Fired when a client fails to authenticate to an Event Store server. 5 | */ 6 | public class AuthenticationFailed implements Event { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/event/ClientReconnecting.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.event; 2 | 3 | /** 4 | * Fired when a client is attempting to reconnect to an Event Store server following a disconnection. 5 | */ 6 | public class ClientReconnecting implements Event { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/VolatileSubscriptionListener.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * The listener interface for receiving volatile subscription action events. 5 | */ 6 | public interface VolatileSubscriptionListener extends SubscriptionListener { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/user/dto/UserHolder.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.user.dto; 2 | 3 | import com.github.msemys.esjc.user.User; 4 | 5 | public class UserHolder { 6 | public final User data; 7 | 8 | public UserHolder(User data) { 9 | this.data = data; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/user/dto/ResetPasswordDetails.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.user.dto; 2 | 3 | public class ResetPasswordDetails { 4 | public final String newPassword; 5 | 6 | public ResetPasswordDetails(String newPassword) { 7 | this.newPassword = newPassword; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/PersistentSubscriptionListener.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * The listener interface for receiving persistent subscription action events. 5 | */ 6 | public interface PersistentSubscriptionListener extends SubscriptionListener { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/node/EndpointDiscoverer.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.node; 2 | 3 | import java.net.InetSocketAddress; 4 | import java.util.concurrent.CompletableFuture; 5 | 6 | public interface EndpointDiscoverer { 7 | 8 | CompletableFuture discover(InetSocketAddress failedTcpEndpoint); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/util/Throwables.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.util; 2 | 3 | public class Throwables { 4 | 5 | public static RuntimeException propagate(Throwable throwable) { 6 | return (throwable instanceof RuntimeException) ? (RuntimeException) throwable : new RuntimeException(throwable); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/node/cluster/VNodeState.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.node.cluster; 2 | 3 | public enum VNodeState { 4 | Initializing, 5 | Unknown, 6 | PreReplica, 7 | CatchingUp, 8 | Clone, 9 | Slave, 10 | PreMaster, 11 | Master, 12 | Manager, 13 | ShuttingDown, 14 | Shutdown 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/operation/NoResultException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.operation; 2 | 3 | import com.github.msemys.esjc.EventStoreException; 4 | 5 | /** 6 | * Exception thrown if there is no result for an operation for which one is expected. 7 | */ 8 | public class NoResultException extends EventStoreException { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/util/Threads.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.util; 2 | 3 | public class Threads { 4 | 5 | public static void sleepUninterruptibly(long millis) { 6 | try { 7 | Thread.sleep(millis); 8 | } catch (InterruptedException e) { 9 | // ignore 10 | } 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/user/dto/UsersHolder.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.user.dto; 2 | 3 | import com.github.msemys.esjc.user.User; 4 | 5 | import java.util.List; 6 | 7 | public class UsersHolder { 8 | public final List data; 9 | 10 | public UsersHolder(List data) { 11 | this.data = data; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/util/Ranges.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.util; 2 | 3 | public class Ranges { 4 | 5 | public static final IntRange BATCH_SIZE_RANGE = new IntRange(1, 4 * 1024, IntRange.Type.CLOSED); 6 | 7 | public static final IntRange ATTEMPTS_RANGE = new IntRange(-1, Integer.MAX_VALUE, IntRange.Type.CLOSED); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/ReadDirection.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Represents the direction of read operation. 5 | */ 6 | public enum ReadDirection { 7 | 8 | /** 9 | * From beginning to end. 10 | */ 11 | Forward, 12 | 13 | /** 14 | * From end to beginning. 15 | */ 16 | Backward 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/event/ErrorOccurred.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.event; 2 | 3 | /** 4 | * Fired when an internal error occurs. 5 | */ 6 | public class ErrorOccurred implements Event { 7 | 8 | public final Throwable throwable; 9 | 10 | public ErrorOccurred(Throwable throwable) { 11 | this.throwable = throwable; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/operation/InvalidTransactionException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.operation; 2 | 3 | import com.github.msemys.esjc.EventStoreException; 4 | 5 | /** 6 | * Exception thrown if there is an attempt to operate inside a transaction which does not exist. 7 | */ 8 | public class InvalidTransactionException extends EventStoreException { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/node/cluster/ClusterInfoDto.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.node.cluster; 2 | 3 | import java.util.List; 4 | 5 | public class ClusterInfoDto { 6 | public List members; 7 | 8 | public ClusterInfoDto() { 9 | } 10 | 11 | public ClusterInfoDto(List members) { 12 | this.members = members; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/operation/Operation.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.operation; 2 | 3 | import com.github.msemys.esjc.tcp.TcpPackage; 4 | 5 | import java.util.UUID; 6 | 7 | public interface Operation { 8 | 9 | TcpPackage create(UUID correlationId); 10 | 11 | InspectionResult inspect(TcpPackage tcpPackage); 12 | 13 | void fail(Exception exception); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/projection/Projections.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.projection; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Projection list holder 7 | */ 8 | public class Projections { 9 | public final List projections; 10 | 11 | public Projections(List projections) { 12 | this.projections = projections; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/task/EstablishTcpConnection.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.task; 2 | 3 | import com.github.msemys.esjc.node.NodeEndpoints; 4 | 5 | public class EstablishTcpConnection implements Task { 6 | public final NodeEndpoints endpoints; 7 | 8 | public EstablishTcpConnection(NodeEndpoints endpoints) { 9 | this.endpoints = endpoints; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | dist: bionic 3 | os: linux 4 | 5 | jdk: 6 | - openjdk8 7 | 8 | services: 9 | - docker 10 | 11 | before_install: 12 | - sudo apt-get update -qq 13 | - sudo apt-get install -y openssl docker-ce 14 | 15 | before_script: 16 | - ./scripts/generate-ssl-cert.sh 17 | 18 | script: 19 | - sudo docker-compose up -d 20 | - mvn clean install 21 | 22 | cache: 23 | directories: 24 | - $HOME/.m2 -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/user/dto/UserUpdateDetails.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.user.dto; 2 | 3 | import java.util.List; 4 | 5 | public class UserUpdateDetails { 6 | public final String fullName; 7 | public final List groups; 8 | 9 | public UserUpdateDetails(String fullName, List groups) { 10 | this.fullName = fullName; 11 | this.groups = groups; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/tcp/handler/AuthenticationEvent.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.tcp.handler; 2 | 3 | import com.github.msemys.esjc.tcp.handler.AuthenticationHandler.AuthenticationStatus; 4 | 5 | public class AuthenticationEvent { 6 | public final AuthenticationStatus status; 7 | 8 | public AuthenticationEvent(AuthenticationStatus status) { 9 | this.status = status; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/user/dto/ChangePasswordDetails.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.user.dto; 2 | 3 | public class ChangePasswordDetails { 4 | public final String currentPassword; 5 | public final String newPassword; 6 | 7 | public ChangePasswordDetails(String currentPassword, String newPassword) { 8 | this.currentPassword = currentPassword; 9 | this.newPassword = newPassword; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/PersistentSubscriptionDeleteStatus.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Status of a single subscription delete message. 5 | */ 6 | public enum PersistentSubscriptionDeleteStatus { 7 | 8 | /** 9 | * The subscription was deleted successfully 10 | */ 11 | Success, 12 | 13 | /** 14 | * Some failure happened deleting the subscription 15 | */ 16 | Failure 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/event/ClientConnected.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.event; 2 | 3 | import java.net.InetSocketAddress; 4 | 5 | /** 6 | * Fired when a client connects to an Event Store server. 7 | */ 8 | public class ClientConnected implements Event { 9 | 10 | public final InetSocketAddress address; 11 | 12 | public ClientConnected(InetSocketAddress address) { 13 | this.address = address; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/StreamPosition.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Stream position constants. 5 | */ 6 | public class StreamPosition { 7 | 8 | /** 9 | * The first event in a stream. 10 | */ 11 | public static final long START = 0; 12 | 13 | /** 14 | * The last event in the stream. 15 | */ 16 | public static final long END = -1; 17 | 18 | private StreamPosition() { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/SliceReadStatus.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Stream slice read status. 5 | */ 6 | public enum SliceReadStatus { 7 | 8 | /** 9 | * The read was successful. 10 | */ 11 | Success, 12 | 13 | /** 14 | * The stream was not found. 15 | */ 16 | StreamNotFound, 17 | 18 | /** 19 | * The stream has previously existed but is deleted. 20 | */ 21 | StreamDeleted 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/subscription/PersistentSubscriptionProtocol.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.subscription; 2 | 3 | import java.util.List; 4 | import java.util.UUID; 5 | 6 | public interface PersistentSubscriptionProtocol { 7 | 8 | void notifyEventsProcessed(List processedEvents); 9 | 10 | void notifyEventsFailed(List processedEvents, PersistentSubscriptionNakEventAction action, String reason); 11 | 12 | void unsubscribe(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/task/CloseConnection.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.task; 2 | 3 | public class CloseConnection implements Task { 4 | public final String reason; 5 | public final Throwable throwable; 6 | 7 | public CloseConnection(String reason) { 8 | this(reason, null); 9 | } 10 | 11 | public CloseConnection(String reason, Throwable throwable) { 12 | this.reason = reason; 13 | this.throwable = throwable; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/EventStoreListener.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.github.msemys.esjc.event.Event; 4 | 5 | import java.util.EventListener; 6 | 7 | /** 8 | * The listener interface for receiving client events. 9 | */ 10 | public interface EventStoreListener extends EventListener { 11 | 12 | /** 13 | * Invoked when the client event occurs. 14 | * 15 | * @param event client event. 16 | */ 17 | void onEvent(Event event); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | eventstore: 5 | image: eventstore/eventstore:release-5.0.11 6 | ports: 7 | - "7773:7773" 8 | - "7779:7779" 9 | - "2113:2113" 10 | environment: 11 | - EVENTSTORE_MEM_DB=true 12 | - EVENTSTORE_RUN_PROJECTIONS=all 13 | - EVENTSTORE_STATS_PERIOD_SEC=3000 14 | - EVENTSTORE_EXT_TCP_PORT=7773 15 | - EVENTSTORE_EXT_SECURE_TCP_PORT=7779 16 | - EVENTSTORE_CERTIFICATE_FILE=/ssl/domain.p12 17 | volumes: 18 | - ./ssl:/ssl 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/WriteStatus.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Write operation status. 5 | */ 6 | public enum WriteStatus { 7 | 8 | /** 9 | * The write operation was successful. 10 | */ 11 | Success, 12 | 13 | /** 14 | * The expected version does not match actual stream version. 15 | */ 16 | WrongExpectedVersion, 17 | 18 | /** 19 | * The stream has previously existed but is deleted. 20 | */ 21 | StreamDeleted 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/rule/Retryable.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.rule; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface Retryable { 11 | 12 | Class value(); 13 | 14 | int maxAttempts() default 3; 15 | 16 | long delay() default 1000; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/util/Numbers.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.util; 2 | 3 | public class Numbers { 4 | 5 | public static boolean isPositive(int value) { 6 | return value > 0; 7 | } 8 | 9 | public static boolean isPositive(long value) { 10 | return value > 0; 11 | } 12 | 13 | public static boolean isNegative(int value) { 14 | return value < 0; 15 | } 16 | 17 | public static boolean isNegative(long value) { 18 | return value < 0; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/PersistentSubscriptionCreateStatus.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Status of a single subscription create message. 5 | */ 6 | public enum PersistentSubscriptionCreateStatus { 7 | 8 | /** 9 | * The subscription was created successfully. 10 | */ 11 | Success, 12 | 13 | /** 14 | * The subscription already exists. 15 | */ 16 | NotFound, 17 | 18 | /** 19 | * Some failure happened creating the subscription. 20 | */ 21 | Failure 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/PersistentSubscriptionCreateResult.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Result type of a single operation creating a persistent subscription in the Event Store. 5 | */ 6 | public class PersistentSubscriptionCreateResult { 7 | 8 | /** 9 | * Status of create attempt. 10 | */ 11 | public final PersistentSubscriptionCreateStatus status; 12 | 13 | public PersistentSubscriptionCreateResult(PersistentSubscriptionCreateStatus status) { 14 | this.status = status; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/PersistentSubscriptionDeleteResult.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Result type of a single operation deleting a persistent subscription in the Event Store. 5 | */ 6 | public class PersistentSubscriptionDeleteResult { 7 | 8 | /** 9 | * Status of delete attempt 10 | */ 11 | public final PersistentSubscriptionDeleteStatus status; 12 | 13 | public PersistentSubscriptionDeleteResult(PersistentSubscriptionDeleteStatus status) { 14 | this.status = status; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/PersistentSubscriptionUpdateResult.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Result type of a single operation updating a persistent subscription in the Event Store. 5 | */ 6 | public class PersistentSubscriptionUpdateResult { 7 | 8 | /** 9 | * Status of update attempt. 10 | */ 11 | public final PersistentSubscriptionUpdateStatus status; 12 | 13 | public PersistentSubscriptionUpdateResult(PersistentSubscriptionUpdateStatus status) { 14 | this.status = status; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/node/cluster/NodePreference.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.node.cluster; 2 | 3 | /** 4 | * Indicates which order of preferred nodes for connecting to. 5 | */ 6 | public enum NodePreference { 7 | 8 | /** 9 | * When attempting connection, prefers master node. 10 | */ 11 | Master, 12 | 13 | /** 14 | * When attempting connection, prefers slave node. 15 | */ 16 | Slave, 17 | 18 | /** 19 | * When attempting connection, has no node preference. 20 | */ 21 | Random 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/EventReadStatus.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Single event read operation status. 5 | */ 6 | public enum EventReadStatus { 7 | 8 | /** 9 | * The read operation was successful. 10 | */ 11 | Success, 12 | 13 | /** 14 | * The event was not found. 15 | */ 16 | NotFound, 17 | 18 | /** 19 | * The stream was not found. 20 | */ 21 | NoStream, 22 | 23 | /** 24 | * The stream previously existed but was deleted. 25 | */ 26 | StreamDeleted 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/subscription/MaximumSubscribersReachedException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.subscription; 2 | 3 | import com.github.msemys.esjc.EventStoreException; 4 | 5 | /** 6 | * Thrown when maximum subscribers is set on subscription and it has been reached. 7 | */ 8 | public class MaximumSubscribersReachedException extends EventStoreException { 9 | 10 | /** 11 | * Creates a new instance. 12 | */ 13 | public MaximumSubscribersReachedException() { 14 | super("Maximum subscriptions reached."); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/system/SystemEventTypes.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.system; 2 | 3 | /** 4 | * System event types. 5 | */ 6 | public class SystemEventTypes { 7 | 8 | public static final String STREAM_DELETED = "$streamDeleted"; 9 | public static final String STATS_COLLECTED = "$statsCollected"; 10 | public static final String LINK_TO = "$>"; 11 | public static final String STREAM_METADATA = "$metadata"; 12 | public static final String SETTINGS = "$settings"; 13 | 14 | private SystemEventTypes() { 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/operation/ServerErrorException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.operation; 2 | 3 | import com.github.msemys.esjc.EventStoreException; 4 | 5 | /** 6 | * Exception thrown if a server-side error occurs during an operation. 7 | */ 8 | public class ServerErrorException extends EventStoreException { 9 | 10 | /** 11 | * Creates a new instance with the specified error message. 12 | * 13 | * @param message error message. 14 | */ 15 | public ServerErrorException(String message) { 16 | super(message); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/subscription/PersistentSubscriptionDeletedException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.subscription; 2 | 3 | import com.github.msemys.esjc.EventStoreException; 4 | 5 | /** 6 | * Thrown when the persistent subscription has been deleted to subscribers connected to it. 7 | */ 8 | public class PersistentSubscriptionDeletedException extends EventStoreException { 9 | 10 | /** 11 | * Creates a new instance. 12 | */ 13 | public PersistentSubscriptionDeletedException() { 14 | super("The subscription has been deleted."); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/operation/AccessDeniedException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.operation; 2 | 3 | import com.github.msemys.esjc.EventStoreException; 4 | 5 | /** 6 | * Exception thrown when a user is not authorised to carry out an operation. 7 | */ 8 | public class AccessDeniedException extends EventStoreException { 9 | 10 | /** 11 | * Creates a new instance with the specified error message. 12 | * 13 | * @param message error message. 14 | */ 15 | public AccessDeniedException(String message) { 16 | super(message); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/operation/manager/OperationTimeoutException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.operation.manager; 2 | 3 | import com.github.msemys.esjc.EventStoreException; 4 | 5 | /** 6 | * Exception thrown if an operation times out. 7 | */ 8 | public class OperationTimeoutException extends EventStoreException { 9 | 10 | /** 11 | * Creates a new instance with the specified error message. 12 | * 13 | * @param message error message. 14 | */ 15 | public OperationTimeoutException(String message) { 16 | super(message); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/DeleteResult.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Result type returned after deleting a stream. 5 | */ 6 | public class DeleteResult { 7 | 8 | /** 9 | * The position of the write in the log. 10 | */ 11 | public final Position logPosition; 12 | 13 | /** 14 | * Creates a new instance with the specified log position. 15 | * 16 | * @param logPosition the position of the write in the log. 17 | */ 18 | public DeleteResult(Position logPosition) { 19 | this.logPosition = logPosition; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/tcp/TcpPackageEncoder.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.tcp; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import io.netty.handler.codec.MessageToMessageEncoder; 5 | 6 | import java.util.List; 7 | 8 | import static io.netty.buffer.Unpooled.wrappedBuffer; 9 | 10 | public class TcpPackageEncoder extends MessageToMessageEncoder { 11 | 12 | @Override 13 | protected void encode(ChannelHandlerContext ctx, TcpPackage msg, List out) throws Exception { 14 | out.add(wrappedBuffer(msg.toByteArray())); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/user/dto/UserCreateDetails.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.user.dto; 2 | 3 | import java.util.List; 4 | 5 | public class UserCreateDetails { 6 | public final String loginName; 7 | public final String fullName; 8 | public final String password; 9 | public final List groups; 10 | 11 | public UserCreateDetails(String loginName, String fullName, String password, List groups) { 12 | this.loginName = loginName; 13 | this.fullName = fullName; 14 | this.password = password; 15 | this.groups = groups; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/CatchUpSubscriptionListener.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * The listener interface for receiving catch-up subscription action events. 5 | */ 6 | public interface CatchUpSubscriptionListener extends SubscriptionListener { 7 | 8 | /** 9 | * Invoked when the subscription switches from the reading phase to the live subscription phase. 10 | * 11 | * @param subscription target subscription. 12 | */ 13 | default void onLiveProcessingStarted(CatchUpSubscription subscription) { 14 | 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/subscription/SubscriptionBufferOverflowException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.subscription; 2 | 3 | import com.github.msemys.esjc.EventStoreException; 4 | 5 | /** 6 | * Thrown when subscription reaches buffer limit. 7 | */ 8 | public class SubscriptionBufferOverflowException extends EventStoreException { 9 | 10 | /** 11 | * Creates a new instance with the specified error message. 12 | * 13 | * @param message error message. 14 | */ 15 | public SubscriptionBufferOverflowException(String message) { 16 | super(message); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/operation/NotAuthenticatedException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.operation; 2 | 3 | import com.github.msemys.esjc.EventStoreException; 4 | 5 | /** 6 | * Exception thrown if an operation requires authentication but the client is not authenticated. 7 | */ 8 | public class NotAuthenticatedException extends EventStoreException { 9 | 10 | /** 11 | * Creates a new instance with the specified error message. 12 | * 13 | * @param message error message. 14 | */ 15 | public NotAuthenticatedException(String message) { 16 | super(message); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/projection/ProjectionMode.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.projection; 2 | 3 | /** 4 | * Projection mode. 5 | */ 6 | public enum ProjectionMode { 7 | 8 | /** 9 | * Transient (ad-hoc) projection runs until completion and is automatically deleted afterwards. 10 | */ 11 | TRANSIENT, 12 | 13 | /** 14 | * One-time projection runs until completion and then stops. 15 | */ 16 | ONE_TIME, 17 | 18 | /** 19 | * Continuous projection runs continuously unless disabled or an unrecoverable error has been encountered. 20 | */ 21 | CONTINUOUS 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/task/StartOperation.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.task; 2 | 3 | import com.github.msemys.esjc.operation.Operation; 4 | 5 | import static com.github.msemys.esjc.util.Preconditions.checkNotNull; 6 | 7 | public class StartOperation implements Task { 8 | public final Operation operation; 9 | 10 | public StartOperation(Operation operation) { 11 | checkNotNull(operation, "operation is null"); 12 | this.operation = operation; 13 | } 14 | 15 | @Override 16 | public void fail(Exception exception) { 17 | operation.fail(exception); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/http/HttpOperationTimeoutException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.http; 2 | 3 | import io.netty.handler.codec.http.HttpRequest; 4 | 5 | /** 6 | * Exception thrown if HTTP operation times out. 7 | */ 8 | public class HttpOperationTimeoutException extends HttpClientException { 9 | 10 | public HttpOperationTimeoutException(String message) { 11 | super(message); 12 | } 13 | 14 | public HttpOperationTimeoutException(HttpRequest request) { 15 | super(String.format("%s %s request never got response from server.", request.method().name(), request.uri())); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/tcp/TcpFlag.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.tcp; 2 | 3 | public enum TcpFlag { 4 | 5 | None(0x00), 6 | 7 | Authenticated(0x01); 8 | 9 | public final byte value; 10 | 11 | TcpFlag(int value) { 12 | this.value = (byte) value; 13 | } 14 | 15 | public static TcpFlag of(byte value) { 16 | for (TcpFlag f : TcpFlag.values()) { 17 | if (f.value == value) { 18 | return f; 19 | } 20 | } 21 | throw new IllegalArgumentException(String.format("Unsupported TCP flag %s", Integer.toHexString(value))); 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/runner/EventStoreRunnerWithParametersFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.runner; 2 | 3 | import org.junit.runner.Runner; 4 | import org.junit.runners.model.InitializationError; 5 | import org.junit.runners.parameterized.ParametersRunnerFactory; 6 | import org.junit.runners.parameterized.TestWithParameters; 7 | 8 | public class EventStoreRunnerWithParametersFactory implements ParametersRunnerFactory { 9 | 10 | @Override 11 | public Runner createRunnerForTestWithParameters(TestWithParameters test) throws InitializationError { 12 | return new EventStoreRunnerWithParameters(test); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/PersistentSubscriptionUpdateStatus.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Status of a single subscription update message. 5 | */ 6 | public enum PersistentSubscriptionUpdateStatus { 7 | 8 | /** 9 | * The subscription was updated successfully. 10 | */ 11 | Success, 12 | 13 | /** 14 | * The subscription already exists. 15 | */ 16 | NotFound, 17 | 18 | /** 19 | * Some failure happened updating the subscription. 20 | */ 21 | Failure, 22 | 23 | /** 24 | * You do not have permissions to update this subscription. 25 | */ 26 | AccessDenied 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/node/EndpointDiscovererFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.node; 2 | 3 | import com.github.msemys.esjc.Settings; 4 | 5 | import java.util.concurrent.ScheduledExecutorService; 6 | 7 | /** 8 | * Endpoint discoverer factory. 9 | */ 10 | public interface EndpointDiscovererFactory { 11 | 12 | /** 13 | * Creates endpoint discoverer 14 | * 15 | * @param settings client settings. 16 | * @param scheduler scheduled executor service that could be used to discover endpoint. 17 | * @return endpoint discoverer 18 | */ 19 | EndpointDiscoverer create(Settings settings, ScheduledExecutorService scheduler); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/util/UUIDConverter.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.util; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.util.UUID; 5 | 6 | public class UUIDConverter { 7 | 8 | public static byte[] toBytes(UUID uuid) { 9 | ByteBuffer bb = ByteBuffer.allocate(16); 10 | bb.putLong(uuid.getMostSignificantBits()); 11 | bb.putLong(uuid.getLeastSignificantBits()); 12 | return bb.array(); 13 | } 14 | 15 | public static UUID toUUID(byte[] bytes) { 16 | ByteBuffer bb = ByteBuffer.wrap(bytes); 17 | long mostSignificantBits = bb.getLong(); 18 | long leastSignificantBits = bb.getLong(); 19 | return new UUID(mostSignificantBits, leastSignificantBits); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/http/HttpClientException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.http; 2 | 3 | /** 4 | * HTTP client exception. 5 | */ 6 | public class HttpClientException extends RuntimeException { 7 | 8 | /** 9 | * Creates a new instance with the specified error message. 10 | * 11 | * @param message error message. 12 | */ 13 | public HttpClientException(String message) { 14 | super(message); 15 | } 16 | 17 | /** 18 | * Creates a new instance with the specified error message and cause. 19 | * 20 | * @param message error message. 21 | * @param cause the cause. 22 | */ 23 | public HttpClientException(String message, Throwable cause) { 24 | super(message, cause); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/subscription/PersistentSubscriptionNakEventAction.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.subscription; 2 | 3 | /** 4 | * Actions to be taken by server in the case of a client NAK. 5 | */ 6 | public enum PersistentSubscriptionNakEventAction { 7 | 8 | /** 9 | * Client unknown on action. Let server decide. 10 | */ 11 | Unknown, 12 | 13 | /** 14 | * Park message - do not resend. Put on poison queue. 15 | */ 16 | Park, 17 | 18 | /** 19 | * Explicitly retry the message. 20 | */ 21 | Retry, 22 | 23 | /** 24 | * Skip this message - do not resend, do not put in poison queue. 25 | */ 26 | Skip, 27 | 28 | /** 29 | * Stop the subscription. 30 | */ 31 | Stop 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/subscription/VolatileSubscription.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.subscription; 2 | 3 | import com.github.msemys.esjc.Subscription; 4 | 5 | public class VolatileSubscription extends Subscription { 6 | 7 | private final VolatileSubscriptionOperation operation; 8 | 9 | public VolatileSubscription(VolatileSubscriptionOperation operation, 10 | String streamId, 11 | long lastCommitPosition, 12 | Long lastEventNumber) { 13 | super(streamId, lastCommitPosition, lastEventNumber); 14 | this.operation = operation; 15 | } 16 | 17 | @Override 18 | public void unsubscribe() { 19 | operation.unsubscribe(); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /scripts/generate-ssl-cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | GIT_ROOT=$(git rev-parse --show-toplevel) 4 | mkdir -p ${GIT_ROOT}/ssl 5 | pushd ${GIT_ROOT}/ssl 6 | 7 | # generate CA-signed certificate 8 | openssl genrsa -out rootCA.key 2048 9 | openssl req -x509 -sha256 -new -nodes -subj "/CN=root" -key rootCA.key -days 365 -out rootCA.crt 10 | openssl genrsa -out domain.key 2048 11 | openssl req -new -sha256 -nodes -key domain.key -subj "/CN=localhost" -out domain.csr 12 | openssl x509 -req -in domain.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out domain.crt -days 365 -sha256 13 | openssl req -x509 -sha256 -nodes -days 365 -subj "/CN=localhost" -newkey rsa:2048 -keyout invalid.key -out invalid.crt 14 | openssl pkcs12 -passout "pass:" -export -inkey domain.key -in domain.crt -out domain.p12 15 | popd 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/util/Iterables.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.util; 2 | 3 | import java.util.Collection; 4 | import java.util.Iterator; 5 | import java.util.Queue; 6 | import java.util.function.Consumer; 7 | 8 | public class Iterables { 9 | 10 | public static void consume(Collection collection, Consumer consumer) { 11 | Iterator iterator = collection.iterator(); 12 | 13 | while (iterator.hasNext()) { 14 | consumer.accept(iterator.next()); 15 | iterator.remove(); 16 | } 17 | } 18 | 19 | public static void consume(Queue queue, Consumer consumer) { 20 | T item; 21 | while ((item = queue.poll()) != null) { 22 | consumer.accept(item); 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/task/StartConnection.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.task; 2 | 3 | import com.github.msemys.esjc.node.EndpointDiscoverer; 4 | 5 | import java.util.concurrent.CompletableFuture; 6 | 7 | import static com.github.msemys.esjc.util.Preconditions.checkNotNull; 8 | 9 | public class StartConnection implements Task { 10 | public final CompletableFuture result; 11 | public final EndpointDiscoverer endpointDiscoverer; 12 | 13 | public StartConnection(CompletableFuture result, EndpointDiscoverer endpointDiscoverer) { 14 | checkNotNull(result, "result is null"); 15 | checkNotNull(endpointDiscoverer, "endpointDiscoverer is null"); 16 | 17 | this.result = result; 18 | this.endpointDiscoverer = endpointDiscoverer; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/subscription/SubscriptionOperation.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.subscription; 2 | 3 | import com.github.msemys.esjc.SubscriptionDropReason; 4 | import com.github.msemys.esjc.operation.InspectionResult; 5 | import com.github.msemys.esjc.tcp.TcpPackage; 6 | import io.netty.channel.Channel; 7 | 8 | import java.util.UUID; 9 | 10 | public interface SubscriptionOperation { 11 | 12 | boolean subscribe(UUID correlationId, Channel connection); 13 | 14 | default void drop(SubscriptionDropReason reason, Exception exception) { 15 | drop(reason, exception, null); 16 | } 17 | 18 | void drop(SubscriptionDropReason reason, Exception exception, Channel connection); 19 | 20 | InspectionResult inspect(TcpPackage tcpPackage); 21 | 22 | void connectionClosed(); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/transaction/TransactionManager.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.transaction; 2 | 3 | import com.github.msemys.esjc.EventData; 4 | import com.github.msemys.esjc.Transaction; 5 | import com.github.msemys.esjc.WriteResult; 6 | import com.github.msemys.esjc.UserCredentials; 7 | 8 | import java.util.concurrent.CompletableFuture; 9 | 10 | public interface TransactionManager { 11 | 12 | default CompletableFuture write(Transaction transaction, Iterable events) { 13 | return write(transaction, events, null); 14 | } 15 | 16 | CompletableFuture write(Transaction transaction, Iterable events, UserCredentials userCredentials); 17 | 18 | CompletableFuture commit(Transaction transaction, UserCredentials userCredentials); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/ConnectionClosedException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Exception thrown by ongoing operations which are terminated by connection closing. 5 | */ 6 | public class ConnectionClosedException extends EventStoreException { 7 | 8 | /** 9 | * Creates a new instance with the specified error message. 10 | * 11 | * @param message error message. 12 | */ 13 | public ConnectionClosedException(String message) { 14 | super(message); 15 | } 16 | 17 | /** 18 | * Creates a new instance with the specified error message and cause. 19 | * 20 | * @param message error message. 21 | * @param cause the cause. 22 | */ 23 | public ConnectionClosedException(String message, Throwable cause) { 24 | super(message, cause); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/user/RelLink.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.user; 2 | 3 | /** 4 | * Represents hypermedia link describing action allowed on user resource. 5 | */ 6 | public class RelLink { 7 | 8 | /** 9 | * Location of the resource. 10 | */ 11 | public final String href; 12 | 13 | /** 14 | * Relationship. 15 | */ 16 | public final String rel; 17 | 18 | public RelLink(String href, String rel) { 19 | this.href = href; 20 | this.rel = rel; 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | final StringBuilder sb = new StringBuilder("RelLink{"); 26 | sb.append("href='").append(href).append('\''); 27 | sb.append(", rel='").append(rel).append('\''); 28 | sb.append('}'); 29 | return sb.toString(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/node/cluster/ClusterException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.node.cluster; 2 | 3 | import com.github.msemys.esjc.EventStoreException; 4 | 5 | /** 6 | * Exception thrown if cluster discovery fails. 7 | */ 8 | public class ClusterException extends EventStoreException { 9 | 10 | /** 11 | * Creates a new instance with the specified error message. 12 | * 13 | * @param message error message. 14 | */ 15 | public ClusterException(String message) { 16 | super(message); 17 | } 18 | 19 | /** 20 | * Creates a new instance with the specified error message and cause. 21 | * 22 | * @param message error message. 23 | * @param cause the cause. 24 | */ 25 | public ClusterException(String message, Throwable cause) { 26 | super(message, cause); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/RetryableResolvedEvent.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.github.msemys.esjc.proto.EventStoreClientMessages; 4 | 5 | /** 6 | * Structure represents a single event or an resolved link event with retry count. 7 | */ 8 | public class RetryableResolvedEvent extends ResolvedEvent { 9 | 10 | /** 11 | * The number of times the event is being retried. 12 | */ 13 | public final Integer retryCount; 14 | 15 | /** 16 | * Creates new instance from proto message. 17 | * 18 | * @param event resolved indexed event. 19 | * @param retryCount the number of times the event is being retried. 20 | */ 21 | public RetryableResolvedEvent(EventStoreClientMessages.ResolvedIndexedEvent event, Integer retryCount) { 22 | super(event); 23 | this.retryCount = retryCount; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/CannotEstablishConnectionException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Exception thrown if client is unable to establish a connection to an Event Store server. 5 | */ 6 | public class CannotEstablishConnectionException extends EventStoreException { 7 | 8 | /** 9 | * Creates a new instance with the specified error message. 10 | * 11 | * @param message error message. 12 | */ 13 | public CannotEstablishConnectionException(String message) { 14 | super(message); 15 | } 16 | 17 | /** 18 | * Creates a new instance with the specified error message and cause. 19 | * 20 | * @param message error message. 21 | * @param cause the cause. 22 | */ 23 | public CannotEstablishConnectionException(String message, Throwable cause) { 24 | super(message, cause); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/node/DelegatedEndpointDiscovererFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.node; 2 | 3 | import com.github.msemys.esjc.Settings; 4 | 5 | import java.util.concurrent.ScheduledExecutorService; 6 | 7 | import static com.github.msemys.esjc.util.Preconditions.checkNotNull; 8 | 9 | /** 10 | * Delegated endpoint discoverer factory. 11 | */ 12 | public class DelegatedEndpointDiscovererFactory implements EndpointDiscovererFactory { 13 | 14 | private final EndpointDiscoverer discoverer; 15 | 16 | public DelegatedEndpointDiscovererFactory(EndpointDiscoverer discoverer) { 17 | checkNotNull(discoverer, "discoverer is null"); 18 | this.discoverer = discoverer; 19 | } 20 | 21 | @Override 22 | public EndpointDiscoverer create(Settings settings, ScheduledExecutorService scheduler) { 23 | return discoverer; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/WriteResult.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Result type returned after writing to a stream. 5 | */ 6 | public class WriteResult { 7 | 8 | /** 9 | * The next expected version for the stream. 10 | */ 11 | public final long nextExpectedVersion; 12 | 13 | /** 14 | * The position of the write in the log. 15 | */ 16 | public final Position logPosition; 17 | 18 | /** 19 | * Creates a new instance with the specified next expected stream version and log position. 20 | * 21 | * @param nextExpectedVersion the next expected version for the stream. 22 | * @param logPosition the position of the write in the log. 23 | */ 24 | public WriteResult(long nextExpectedVersion, Position logPosition) { 25 | this.nextExpectedVersion = nextExpectedVersion; 26 | this.logPosition = logPosition; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/node/NodeEndpoints.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.node; 2 | 3 | import java.net.InetSocketAddress; 4 | 5 | import static com.github.msemys.esjc.util.Preconditions.checkArgument; 6 | 7 | public class NodeEndpoints { 8 | public final InetSocketAddress tcpEndpoint; 9 | public final InetSocketAddress secureTcpEndpoint; 10 | 11 | public NodeEndpoints(InetSocketAddress tcpEndpoint, InetSocketAddress secureTcpEndpoint) { 12 | checkArgument(tcpEndpoint != null || secureTcpEndpoint != null, "Both endpoints are null."); 13 | this.tcpEndpoint = tcpEndpoint; 14 | this.secureTcpEndpoint = secureTcpEndpoint; 15 | } 16 | 17 | @Override 18 | public String toString() { 19 | return String.format("[%s, %s]", 20 | tcpEndpoint == null ? "n/a" : tcpEndpoint, 21 | secureTcpEndpoint == null ? "n/a" : secureTcpEndpoint); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/AllEventsIterator.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.CompletableFuture; 5 | import java.util.function.Function; 6 | 7 | /** 8 | * $all stream events iterator. 9 | */ 10 | public class AllEventsIterator extends AbstractEventsIterator { 11 | 12 | AllEventsIterator(Position position, Function> reader) { 13 | super(position, reader); 14 | } 15 | 16 | @Override 17 | protected Position getNextCursor(AllEventsSlice slice) { 18 | return slice.nextPosition; 19 | } 20 | 21 | @Override 22 | protected List getEvents(AllEventsSlice slice) { 23 | return slice.events; 24 | } 25 | 26 | @Override 27 | protected boolean isEndOfStream(AllEventsSlice slice) { 28 | return slice.isEndOfStream(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/tcp/TcpPackageDecoder.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.tcp; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.handler.codec.MessageToMessageDecoder; 6 | 7 | import java.util.List; 8 | 9 | public class TcpPackageDecoder extends MessageToMessageDecoder { 10 | 11 | @Override 12 | protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List out) throws Exception { 13 | final TcpPackage tcpPackage; 14 | 15 | if (msg.hasArray()) { 16 | tcpPackage = TcpPackage.of(msg.array()); 17 | } else { 18 | int length = msg.readableBytes(); 19 | byte[] array = new byte[length]; 20 | 21 | msg.getBytes(msg.readerIndex(), array, 0, length); 22 | 23 | tcpPackage = TcpPackage.of(array); 24 | } 25 | 26 | out.add(tcpPackage); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/WriteAttemptResult.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Result type with operation status returned after writing to a stream. 5 | */ 6 | public class WriteAttemptResult extends WriteResult { 7 | 8 | /** 9 | * The status of the write operation. 10 | */ 11 | public final WriteStatus status; 12 | 13 | /** 14 | * Creates a new instance with the specified next expected stream version, log position and operation status. 15 | * 16 | * @param nextExpectedVersion the next expected version for the stream. 17 | * @param logPosition the position of the write in the log. 18 | * @param status the status of the write operation. 19 | */ 20 | public WriteAttemptResult(long nextExpectedVersion, Position logPosition, WriteStatus status) { 21 | super(nextExpectedVersion, logPosition); 22 | this.status = status; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/AllEventsSpliterator.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.CompletableFuture; 5 | import java.util.function.Function; 6 | 7 | /** 8 | * $all stream events spliterator. 9 | */ 10 | public class AllEventsSpliterator extends AbstractEventsSpliterator { 11 | 12 | AllEventsSpliterator(Position position, Function> reader) { 13 | super(position, reader); 14 | } 15 | 16 | @Override 17 | protected Position getNextCursor(AllEventsSlice slice) { 18 | return slice.nextPosition; 19 | } 20 | 21 | @Override 22 | protected List getEvents(AllEventsSlice slice) { 23 | return slice.events; 24 | } 25 | 26 | @Override 27 | protected boolean isEndOfStream(AllEventsSlice slice) { 28 | return slice.isEndOfStream(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/operation/StreamDeletedException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.operation; 2 | 3 | import com.github.msemys.esjc.EventStoreException; 4 | 5 | /** 6 | * Exception thrown if an operation is attempted on a stream which has been deleted. 7 | */ 8 | public class StreamDeletedException extends EventStoreException { 9 | public final String stream; 10 | 11 | /** 12 | * Creates a new instance. 13 | */ 14 | public StreamDeletedException() { 15 | super("Transaction failed due to underlying stream being deleted."); 16 | this.stream = null; 17 | } 18 | 19 | /** 20 | * Creates a new instance with the specified name of deleted stream. 21 | * 22 | * @param stream the name of the deleted stream. 23 | */ 24 | public StreamDeletedException(String stream) { 25 | super(String.format("Event stream '%s' is deleted.", stream)); 26 | this.stream = stream; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/system/SystemStreams.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.system; 2 | 3 | import static com.github.msemys.esjc.util.Preconditions.checkArgument; 4 | 5 | public class SystemStreams { 6 | public static final String SETTINGS_STREAM = "$settings"; 7 | public static final String STATS_STREAM_PREFIX = "$stats"; 8 | public static final String METASTREAM_PREFIX = "$$"; 9 | 10 | private SystemStreams() { 11 | } 12 | 13 | public static String metastreamOf(String streamId) { 14 | return METASTREAM_PREFIX + streamId; 15 | } 16 | 17 | public static boolean isMetastream(String streamId) { 18 | return streamId.startsWith(METASTREAM_PREFIX); 19 | } 20 | 21 | public static String originalStreamOf(String metastreamId) { 22 | checkArgument(isMetastream(metastreamId), "'%s' is not metastream", metastreamId); 23 | return metastreamId.substring(METASTREAM_PREFIX.length()); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/SubscriptionListener.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * The listener interface for receiving subscription action events. 5 | * 6 | * @param subscription type. 7 | * @param event type. 8 | */ 9 | public interface SubscriptionListener { 10 | 11 | /** 12 | * Invoked when a new event is received over the subscription. 13 | * 14 | * @param subscription target subscription. 15 | * @param event event appeared. 16 | */ 17 | void onEvent(T subscription, E event); 18 | 19 | /** 20 | * Invoked when the subscription is dropped. 21 | * 22 | * @param subscription target subscription. 23 | * @param reason subscription drop reason. 24 | * @param exception subscription drop cause (maybe {@code null}) 25 | */ 26 | default void onClose(T subscription, SubscriptionDropReason reason, Exception exception) { 27 | 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/system/SystemConsumerStrategy.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.system; 2 | 3 | /** 4 | * System supported consumer strategies for use with persistent subscriptions. 5 | */ 6 | public enum SystemConsumerStrategy { 7 | 8 | /** 9 | * Distributes events to a single client until it is full. Then round robin to the next client. 10 | */ 11 | DISPATCH_TO_SINGLE("DispatchToSingle"), 12 | 13 | /** 14 | * Distribute events to each client in a round robin fashion. 15 | */ 16 | ROUND_ROBIN("RoundRobin"), 17 | 18 | /** 19 | * Distribute events of the same streamId to the same client until it disconnects on a best efforts basis. 20 | *

21 | * Designed to be used with indexes such as the category projection. 22 | *

23 | */ 24 | PINNED("Pinned"); 25 | 26 | public final String value; 27 | 28 | SystemConsumerStrategy(String value) { 29 | this.value = value; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/util/Strings.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.util; 2 | 3 | import static java.nio.charset.StandardCharsets.UTF_8; 4 | 5 | public class Strings { 6 | public static final String EMPTY = ""; 7 | 8 | public static boolean isNullOrEmpty(String string) { 9 | return string == null || string.isEmpty(); 10 | } 11 | 12 | public static String newString(byte[] bytes) { 13 | return (bytes == null || bytes.length == 0) ? EMPTY : new String(bytes, UTF_8); 14 | } 15 | 16 | public static byte[] toBytes(String string) { 17 | if (string == null) { 18 | return null; 19 | } else if (string.isEmpty()) { 20 | return EmptyArrays.EMPTY_BYTES; 21 | } else { 22 | return string.getBytes(UTF_8); 23 | } 24 | } 25 | 26 | public static String defaultIfEmpty(String string, String defaultString) { 27 | return isNullOrEmpty(string) ? defaultString : string; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/UserCredentials.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import static com.github.msemys.esjc.util.Preconditions.checkNotNull; 4 | 5 | /** 6 | * A username/password pair used for authentication and authorization operations. 7 | */ 8 | public class UserCredentials { 9 | 10 | public final String username; 11 | public final String password; 12 | 13 | public UserCredentials(String username, String password) { 14 | checkNotNull(username, "User name is not specified."); 15 | checkNotNull(password, "Password is not specified."); 16 | this.username = username; 17 | this.password = password; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | final StringBuilder sb = new StringBuilder("UserCredentials{"); 23 | sb.append("username='").append(username).append('\''); 24 | sb.append(", password='****").append('\''); 25 | sb.append('}'); 26 | return sb.toString(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/operation/manager/RetriesLimitReachedException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.operation.manager; 2 | 3 | import com.github.msemys.esjc.EventStoreException; 4 | 5 | /** 6 | * Exception thrown if the number of retries for an operation is reached. 7 | */ 8 | public class RetriesLimitReachedException extends EventStoreException { 9 | 10 | /** 11 | * Creates a new instance. 12 | * 13 | * @param retries the number of retries attempted. 14 | */ 15 | public RetriesLimitReachedException(int retries) { 16 | super(String.format("Reached retries limit : %d", retries)); 17 | } 18 | 19 | /** 20 | * Creates a new instance. 21 | * 22 | * @param item the name of the item for which retries were attempted. 23 | * @param retries the number of retries attempted. 24 | */ 25 | public RetriesLimitReachedException(String item, int retries) { 26 | super(String.format("Item %s reached retries limit : %d", item, retries)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/ITWhenWorkingWithMetadata.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.github.msemys.esjc.util.EmptyArrays; 4 | import org.junit.Test; 5 | 6 | import static org.junit.Assert.*; 7 | 8 | public class ITWhenWorkingWithMetadata extends AbstractEventStoreTest { 9 | 10 | public ITWhenWorkingWithMetadata(EventStore eventstore) { 11 | super(eventstore); 12 | } 13 | 14 | @Test 15 | public void getsMetadataForAnExistingStreamAndNoMetadataExists() { 16 | final String stream = generateStreamName(); 17 | 18 | eventstore.appendToStream(stream, ExpectedVersion.NO_STREAM, newTestEvent()).join(); 19 | 20 | RawStreamMetadataResult metadata = eventstore.getStreamMetadataAsRawBytes(stream).join(); 21 | assertEquals(stream, metadata.stream); 22 | assertFalse(metadata.isStreamDeleted); 23 | assertEquals(-1, metadata.metastreamVersion); 24 | assertArrayEquals(EmptyArrays.EMPTY_BYTES, metadata.streamMetadata); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/operation/CommandNotExpectedException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.operation; 2 | 3 | import com.github.msemys.esjc.EventStoreException; 4 | import com.github.msemys.esjc.tcp.TcpCommand; 5 | 6 | /** 7 | * Exception thrown if an unexpected command is received. 8 | */ 9 | public class CommandNotExpectedException extends EventStoreException { 10 | 11 | /** 12 | * Creates new a new instance. 13 | * 14 | * @param expected expected tcp command. 15 | * @param actual actual tcp command. 16 | */ 17 | public CommandNotExpectedException(TcpCommand expected, TcpCommand actual) { 18 | super(String.format("Expected : %s. Actual : %s.", expected, actual)); 19 | } 20 | 21 | /** 22 | * Creates a new instance. 23 | * 24 | * @param unexpectedCommand unexpected tcp command. 25 | */ 26 | public CommandNotExpectedException(String unexpectedCommand) { 27 | super(String.format("Unexpected command: %s.", unexpectedCommand)); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/user/UserConflictException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.user; 2 | 3 | import io.netty.handler.codec.http.FullHttpResponse; 4 | import io.netty.handler.codec.http.HttpRequest; 5 | 6 | /** 7 | * Exception thrown if a user operation fails with status code {@code 409} (Conflict). 8 | */ 9 | public class UserConflictException extends UserException { 10 | 11 | /** 12 | * Creates a new instance with the specified status code and error message. 13 | * 14 | * @param httpStatusCode HTTP status code. 15 | * @param message error message. 16 | */ 17 | public UserConflictException(int httpStatusCode, String message) { 18 | super(httpStatusCode, message); 19 | } 20 | 21 | /** 22 | * Creates a new instance from the specified HTTP request and response. 23 | * 24 | * @param request HTTP request. 25 | * @param response HTTP response. 26 | */ 27 | public UserConflictException(HttpRequest request, FullHttpResponse response) { 28 | super(request, response); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/user/UserNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.user; 2 | 3 | import io.netty.handler.codec.http.FullHttpResponse; 4 | import io.netty.handler.codec.http.HttpRequest; 5 | 6 | /** 7 | * Exception thrown if a user operation fails with status code {@code 404} (Not Found). 8 | */ 9 | public class UserNotFoundException extends UserException { 10 | 11 | /** 12 | * Creates a new instance with the specified status code and error message. 13 | * 14 | * @param httpStatusCode HTTP status code. 15 | * @param message error message. 16 | */ 17 | public UserNotFoundException(int httpStatusCode, String message) { 18 | super(httpStatusCode, message); 19 | } 20 | 21 | /** 22 | * Creates a new instance from the specified HTTP request and response. 23 | * 24 | * @param request HTTP request. 25 | * @param response HTTP response. 26 | */ 27 | public UserNotFoundException(HttpRequest request, FullHttpResponse response) { 28 | super(request, response); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/operation/StreamNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.operation; 2 | 3 | import com.github.msemys.esjc.EventStoreException; 4 | 5 | /** 6 | * Exception thrown if an operation is attempted on non-existent stream. 7 | */ 8 | public class StreamNotFoundException extends EventStoreException { 9 | public final String stream; 10 | 11 | /** 12 | * Creates a new instance with the specified name of non-existent stream. 13 | * 14 | * @param stream the name of the non-existent stream. 15 | */ 16 | public StreamNotFoundException(String stream) { 17 | this("Event stream '%s' not found.", stream); 18 | } 19 | 20 | /** 21 | * Creates a new instance with the specified error message and name of non-existent stream. 22 | * 23 | * @param message error message 24 | * @param stream the name of the non-existent stream. 25 | */ 26 | public StreamNotFoundException(String message, String stream) { 27 | super(String.format(message, stream)); 28 | this.stream = stream; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/projection/ProjectionConflictException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.projection; 2 | 3 | import io.netty.handler.codec.http.FullHttpResponse; 4 | import io.netty.handler.codec.http.HttpRequest; 5 | 6 | /** 7 | * Exception thrown if a projection operation fails with status code {@code 409} (Conflict). 8 | */ 9 | public class ProjectionConflictException extends ProjectionException { 10 | 11 | /** 12 | * Creates a new instance with the specified status code and error message. 13 | * 14 | * @param httpStatusCode HTTP status code. 15 | * @param message error message. 16 | */ 17 | public ProjectionConflictException(int httpStatusCode, String message) { 18 | super(httpStatusCode, message); 19 | } 20 | 21 | /** 22 | * Creates a new instance from the specified HTTP request and response. 23 | * 24 | * @param request HTTP request. 25 | * @param response HTTP response. 26 | */ 27 | public ProjectionConflictException(HttpRequest request, FullHttpResponse response) { 28 | super(request, response); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/projection/ProjectionNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.projection; 2 | 3 | import io.netty.handler.codec.http.FullHttpResponse; 4 | import io.netty.handler.codec.http.HttpRequest; 5 | 6 | /** 7 | * Exception thrown if a projection operation fails with status code {@code 404} (Not Found). 8 | */ 9 | public class ProjectionNotFoundException extends ProjectionException { 10 | 11 | /** 12 | * Creates a new instance with the specified status code and error message. 13 | * 14 | * @param httpStatusCode HTTP status code. 15 | * @param message error message. 16 | */ 17 | public ProjectionNotFoundException(int httpStatusCode, String message) { 18 | super(httpStatusCode, message); 19 | } 20 | 21 | /** 22 | * Creates a new instance from the specified HTTP request and response. 23 | * 24 | * @param request HTTP request. 25 | * @param response HTTP response. 26 | */ 27 | public ProjectionNotFoundException(HttpRequest request, FullHttpResponse response) { 28 | super(request, response); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mindaugas Šėmys 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/EventStoreException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * {@code EventStoreException} is the superclass of exceptions that is thrown by a client. 5 | */ 6 | public class EventStoreException extends RuntimeException { 7 | 8 | /** 9 | * Creates a new instance. 10 | */ 11 | public EventStoreException() { 12 | } 13 | 14 | /** 15 | * Creates a new instance with the specified error message. 16 | * 17 | * @param message error message. 18 | */ 19 | public EventStoreException(String message) { 20 | super(message); 21 | } 22 | 23 | /** 24 | * Creates a new instance with the specified error message and cause. 25 | * 26 | * @param message error message. 27 | * @param cause the cause. 28 | */ 29 | public EventStoreException(String message, Throwable cause) { 30 | super(message, cause); 31 | } 32 | 33 | /** 34 | * Creates a new instance with the specified cause. 35 | * 36 | * @param cause the cause. 37 | */ 38 | public EventStoreException(Throwable cause) { 39 | super(cause); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/ITReadEventOfLinkToToDeletedEvent.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class ITReadEventOfLinkToToDeletedEvent extends AbstractEventStoreTest { 8 | 9 | public ITReadEventOfLinkToToDeletedEvent(EventStore eventstore) { 10 | super(eventstore); 11 | } 12 | 13 | @Test 14 | public void readsLinkedEvent() { 15 | final String deletedStreamName = generateStreamName(); 16 | final String linkedStreamName = generateStreamName(); 17 | 18 | eventstore.appendToStream(deletedStreamName, ExpectedVersion.ANY, newTestEvent()).join(); 19 | eventstore.appendToStream(linkedStreamName, ExpectedVersion.ANY, newLinkEvent(deletedStreamName, 0)).join(); 20 | eventstore.deleteStream(deletedStreamName, ExpectedVersion.ANY).join(); 21 | 22 | EventReadResult result = eventstore.readEvent(linkedStreamName, 0, true).join(); 23 | 24 | assertNotNull("Missing linked event", result.event.link); 25 | assertNull("Deleted event was resolved", result.event.event); 26 | assertEquals(EventReadStatus.Success, result.status); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/runner/EventStoreRunnerWithParameters.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.runner; 2 | 3 | import com.github.msemys.esjc.EventStore; 4 | import org.junit.runner.notification.RunNotifier; 5 | import org.junit.runners.model.InitializationError; 6 | import org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParameters; 7 | import org.junit.runners.parameterized.TestWithParameters; 8 | 9 | import static com.github.msemys.esjc.util.Preconditions.checkState; 10 | 11 | public class EventStoreRunnerWithParameters extends BlockJUnit4ClassRunnerWithParameters { 12 | 13 | private final EventStore eventstore; 14 | 15 | public EventStoreRunnerWithParameters(TestWithParameters test) throws InitializationError { 16 | super(test); 17 | 18 | Object parameter = test.getParameters().get(0); 19 | 20 | checkState(parameter instanceof EventStore, "test parameter should be type of '%s'", EventStore.class.getName()); 21 | 22 | eventstore = (EventStore) parameter; 23 | } 24 | 25 | @Override 26 | public void run(RunNotifier notifier) { 27 | try { 28 | super.run(notifier); 29 | } finally { 30 | eventstore.shutdown(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/util/concurrent/DefaultThreadFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.util.concurrent; 2 | 3 | import java.util.concurrent.ThreadFactory; 4 | import java.util.concurrent.atomic.AtomicInteger; 5 | 6 | public class DefaultThreadFactory implements ThreadFactory { 7 | private static final AtomicInteger poolNumber = new AtomicInteger(1); 8 | private final ThreadGroup group; 9 | private final AtomicInteger threadNumber = new AtomicInteger(1); 10 | private final String namePrefix; 11 | 12 | public DefaultThreadFactory(String poolName) { 13 | SecurityManager s = System.getSecurityManager(); 14 | group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); 15 | namePrefix = poolName + "-" + poolNumber.getAndIncrement() + "-"; 16 | } 17 | 18 | @Override 19 | public Thread newThread(Runnable r) { 20 | Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); 21 | 22 | if (t.isDaemon()) { 23 | t.setDaemon(false); 24 | } 25 | 26 | if (t.getPriority() != Thread.NORM_PRIORITY) { 27 | t.setPriority(Thread.NORM_PRIORITY); 28 | } 29 | 30 | return t; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/matcher/IteratorSizeMatcher.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.matcher; 2 | 3 | import org.hamcrest.Description; 4 | import org.hamcrest.Factory; 5 | import org.hamcrest.TypeSafeMatcher; 6 | 7 | import java.util.Iterator; 8 | 9 | public class IteratorSizeMatcher extends TypeSafeMatcher> { 10 | private final int expected; 11 | private int actual = 0; 12 | 13 | public IteratorSizeMatcher(int expected) { 14 | this.expected = expected; 15 | } 16 | 17 | @Override 18 | protected boolean matchesSafely(Iterator item) { 19 | while (item.hasNext()) { 20 | item.next(); 21 | actual++; 22 | } 23 | return expected == actual; 24 | } 25 | 26 | @Override 27 | public void describeTo(Description description) { 28 | description.appendText("size ").appendValue(expected); 29 | } 30 | 31 | @Override 32 | protected void describeMismatchSafely(Iterator item, Description description) { 33 | description.appendText("was ").appendValue(actual); 34 | } 35 | 36 | @Factory 37 | public static IteratorSizeMatcher hasSize(int size) { 38 | return new IteratorSizeMatcher(size); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/util/SystemTime.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.util; 2 | 3 | import java.time.Duration; 4 | import java.time.Instant; 5 | 6 | import static com.github.msemys.esjc.util.Preconditions.checkNotNull; 7 | 8 | 9 | public class SystemTime { 10 | private long nanos; 11 | 12 | private SystemTime(long nanos) { 13 | this.nanos = nanos; 14 | } 15 | 16 | public void update() { 17 | nanos = systemNanoTime(); 18 | } 19 | 20 | public long elapsedNanos() { 21 | return systemNanoTime() - nanos; 22 | } 23 | 24 | public boolean isElapsed(Duration duration) { 25 | checkNotNull(duration, "duration is null"); 26 | return elapsedNanos() > duration.toNanos(); 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | Instant instant = nanos > 0 ? Instant.now().minusNanos(elapsedNanos()) : Instant.MIN; 32 | return instant.toString() + "~" + nanos; 33 | } 34 | 35 | public static SystemTime now() { 36 | return new SystemTime(systemNanoTime()); 37 | } 38 | 39 | public static SystemTime zero() { 40 | return new SystemTime(0); 41 | } 42 | 43 | private static long systemNanoTime() { 44 | return System.nanoTime(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/node/DefaultEndpointDiscovererFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.node; 2 | 3 | import com.github.msemys.esjc.Settings; 4 | import com.github.msemys.esjc.node.cluster.ClusterEndpointDiscoverer; 5 | import com.github.msemys.esjc.node.single.SingleEndpointDiscoverer; 6 | 7 | import java.util.concurrent.ScheduledExecutorService; 8 | 9 | import static com.github.msemys.esjc.util.Preconditions.checkNotNull; 10 | 11 | /** 12 | * Default endpoint discoverer factory. 13 | */ 14 | public class DefaultEndpointDiscovererFactory implements EndpointDiscovererFactory { 15 | 16 | @Override 17 | public EndpointDiscoverer create(Settings settings, ScheduledExecutorService scheduler) { 18 | checkNotNull(settings, "settings is null"); 19 | checkNotNull(scheduler, "scheduler is null"); 20 | 21 | if (settings.singleNodeSettings != null) { 22 | return new SingleEndpointDiscoverer(settings.singleNodeSettings, settings.sslSettings.useSslConnection); 23 | } else if (settings.clusterNodeSettings != null) { 24 | return new ClusterEndpointDiscoverer(settings.clusterNodeSettings, scheduler); 25 | } else { 26 | throw new IllegalStateException("Node settings not found"); 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/subscription/PersistentSubscriptionChannel.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.subscription; 2 | 3 | import com.github.msemys.esjc.Subscription; 4 | 5 | import java.util.List; 6 | import java.util.UUID; 7 | 8 | public class PersistentSubscriptionChannel extends Subscription implements PersistentSubscriptionProtocol { 9 | 10 | private final PersistentSubscriptionProtocol protocol; 11 | 12 | public PersistentSubscriptionChannel(PersistentSubscriptionProtocol protocol, 13 | String streamId, 14 | long lastCommitPosition, 15 | Long lastEventNumber) { 16 | super(streamId, lastCommitPosition, lastEventNumber); 17 | this.protocol = protocol; 18 | } 19 | 20 | @Override 21 | public void notifyEventsProcessed(List processedEvents) { 22 | protocol.notifyEventsProcessed(processedEvents); 23 | } 24 | 25 | @Override 26 | public void notifyEventsFailed(List processedEvents, PersistentSubscriptionNakEventAction action, String reason) { 27 | protocol.notifyEventsFailed(processedEvents, action, reason); 28 | } 29 | 30 | @Override 31 | public void unsubscribe() { 32 | protocol.unsubscribe(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/event/Events.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.event; 2 | 3 | import java.net.InetSocketAddress; 4 | 5 | public class Events { 6 | private static final AuthenticationFailed AUTHENTICATION_FAILED_EVENT = new AuthenticationFailed(); 7 | private static final ClientDisconnected CLIENT_DISCONNECTED_EVENT = new ClientDisconnected(); 8 | private static final ClientReconnecting CLIENT_RECONNECTING_EVENT = new ClientReconnecting(); 9 | private static final ConnectionClosed CONNECTION_CLOSED_EVENT = new ConnectionClosed(); 10 | 11 | private Events() { 12 | } 13 | 14 | public static AuthenticationFailed authenticationFailed() { 15 | return AUTHENTICATION_FAILED_EVENT; 16 | } 17 | 18 | public static ClientConnected clientConnected(InetSocketAddress address) { 19 | return new ClientConnected(address); 20 | } 21 | 22 | public static ClientDisconnected clientDisconnected() { 23 | return CLIENT_DISCONNECTED_EVENT; 24 | } 25 | 26 | public static ClientReconnecting clientReconnecting() { 27 | return CLIENT_RECONNECTING_EVENT; 28 | } 29 | 30 | public static ConnectionClosed connectionClosed() { 31 | return CONNECTION_CLOSED_EVENT; 32 | } 33 | 34 | public static ErrorOccurred errorOccurred(Throwable throwable) { 35 | return new ErrorOccurred(throwable); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/util/concurrent/ResettableLatch.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.util.concurrent; 2 | 3 | import com.github.msemys.esjc.util.Throwables; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | import java.util.concurrent.locks.Condition; 7 | import java.util.concurrent.locks.ReentrantLock; 8 | 9 | public class ResettableLatch { 10 | private final ReentrantLock lock = new ReentrantLock(); 11 | private final Condition condition = lock.newCondition(); 12 | private volatile boolean released; 13 | 14 | public ResettableLatch(boolean released) { 15 | this.released = released; 16 | } 17 | 18 | public boolean await(long time, TimeUnit unit) { 19 | lock.lock(); 20 | try { 21 | return released || condition.await(time, unit); 22 | } catch (InterruptedException e) { 23 | throw Throwables.propagate(e); 24 | } finally { 25 | lock.unlock(); 26 | } 27 | } 28 | 29 | public void release() { 30 | lock.lock(); 31 | try { 32 | condition.signal(); 33 | released = true; 34 | } finally { 35 | lock.unlock(); 36 | } 37 | } 38 | 39 | public void reset() { 40 | lock.lock(); 41 | try { 42 | released = false; 43 | } finally { 44 | lock.unlock(); 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/ITReadAllEventsBackwardWithLinkToDeletedEvents.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class ITReadAllEventsBackwardWithLinkToDeletedEvents extends AbstractEventStoreTest { 8 | 9 | public ITReadAllEventsBackwardWithLinkToDeletedEvents(EventStore eventstore) { 10 | super(eventstore); 11 | } 12 | 13 | @Test 14 | public void readsOneEvent() { 15 | final String deletedStreamName = generateStreamName(); 16 | final String linkedStreamName = generateStreamName(); 17 | 18 | eventstore.appendToStream(deletedStreamName, ExpectedVersion.ANY, newTestEvent()).join(); 19 | eventstore.appendToStream(linkedStreamName, ExpectedVersion.ANY, newLinkEvent(deletedStreamName, 0)).join(); 20 | eventstore.deleteStream(deletedStreamName, ExpectedVersion.ANY).join(); 21 | 22 | StreamEventsSlice slice = eventstore.readStreamEventsBackward(linkedStreamName, 0, 1, true).join(); 23 | 24 | assertEquals(1, slice.events.size()); 25 | 26 | ResolvedEvent resolvedEvent = slice.events.get(0); 27 | assertNull("Linked event was resolved", resolvedEvent.event); 28 | assertNotNull("Linked event is not included", resolvedEvent.originalEvent()); 29 | assertFalse("Event was resolved", resolvedEvent.isResolved()); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/ITReadAllEventsForwardWithLinkToToDeletedEvents.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class ITReadAllEventsForwardWithLinkToToDeletedEvents extends AbstractEventStoreTest { 8 | 9 | public ITReadAllEventsForwardWithLinkToToDeletedEvents(EventStore eventstore) { 10 | super(eventstore); 11 | } 12 | 13 | @Test 14 | public void readsOneEvent() { 15 | final String deletedStreamName = generateStreamName(); 16 | final String linkedStreamName = generateStreamName(); 17 | 18 | eventstore.appendToStream(deletedStreamName, ExpectedVersion.ANY, newTestEvent()).join(); 19 | eventstore.appendToStream(linkedStreamName, ExpectedVersion.ANY, newLinkEvent(deletedStreamName, 0)).join(); 20 | eventstore.deleteStream(deletedStreamName, ExpectedVersion.ANY).join(); 21 | 22 | StreamEventsSlice slice = eventstore.readStreamEventsForward(linkedStreamName, 0, 1, true).join(); 23 | 24 | assertEquals(1, slice.events.size()); 25 | 26 | ResolvedEvent resolvedEvent = slice.events.get(0); 27 | assertNull("Linked event was resolved", resolvedEvent.event); 28 | assertNotNull("Linked event is not included", resolvedEvent.originalEvent()); 29 | assertFalse("Event was resolved", resolvedEvent.isResolved()); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/ITSubscribeToAll.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.concurrent.CountDownLatch; 6 | 7 | import static java.util.concurrent.TimeUnit.SECONDS; 8 | import static org.junit.Assert.assertTrue; 9 | 10 | public class ITSubscribeToAll extends AbstractEventStoreTest { 11 | 12 | public ITSubscribeToAll(EventStore eventstore) { 13 | super(eventstore); 14 | } 15 | 16 | @Test 17 | public void allowsMultipleSubscriptions() throws InterruptedException { 18 | final String stream = generateStreamName(); 19 | 20 | CountDownLatch eventSignal = new CountDownLatch(2); 21 | 22 | eventstore.subscribeToAll(false, (s, e) -> eventSignal.countDown()).join(); 23 | eventstore.subscribeToAll(false, (s, e) -> eventSignal.countDown()).join(); 24 | eventstore.appendToStream(stream, ExpectedVersion.NO_STREAM, newTestEvent()).join(); 25 | 26 | assertTrue("onEvent timeout", eventSignal.await(10, SECONDS)); 27 | } 28 | 29 | @Test 30 | public void catchesDeletedEventsAsWell() throws InterruptedException { 31 | final String stream = generateStreamName(); 32 | 33 | CountDownLatch eventSignal = new CountDownLatch(1); 34 | 35 | eventstore.subscribeToAll(false, (s, e) -> eventSignal.countDown()).join(); 36 | eventstore.deleteStream(stream, ExpectedVersion.NO_STREAM, true).join(); 37 | 38 | assertTrue("onEvent timeout", eventSignal.await(10, SECONDS)); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/ITReadStreamEventsForwardWithUnresolvedLinkTo.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class ITReadStreamEventsForwardWithUnresolvedLinkTo extends AbstractEventStoreTest { 8 | 9 | public ITReadStreamEventsForwardWithUnresolvedLinkTo(EventStore eventstore) { 10 | super(eventstore); 11 | } 12 | 13 | @Test 14 | public void readsEventsWithUnresolvedLinkTo() { 15 | final String deletedStreamName = generateStreamName(); 16 | final String linkedStreamName = generateStreamName(); 17 | 18 | eventstore.appendToStream(deletedStreamName, ExpectedVersion.NO_STREAM, newTestEvents(20)).join(); 19 | eventstore.appendToStream(linkedStreamName, ExpectedVersion.NO_STREAM, newLinkEvent(deletedStreamName, 0)).join(); 20 | eventstore.deleteStream(deletedStreamName, ExpectedVersion.ANY).join(); 21 | 22 | StreamEventsSlice deletedStreamSlice = eventstore.readStreamEventsForward(deletedStreamName, 0, 100, false).join(); 23 | assertEquals(SliceReadStatus.StreamNotFound, deletedStreamSlice.status); 24 | assertTrue(deletedStreamSlice.events.isEmpty()); 25 | 26 | StreamEventsSlice linkedStreamSlice = eventstore.readStreamEventsForward(linkedStreamName, 0, 1, true).join(); 27 | assertEquals(1, linkedStreamSlice.events.size()); 28 | assertNull(linkedStreamSlice.events.get(0).event); 29 | assertNotNull(linkedStreamSlice.events.get(0).link); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/user/UserException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.user; 2 | 3 | import io.netty.handler.codec.http.FullHttpResponse; 4 | import io.netty.handler.codec.http.HttpRequest; 5 | 6 | import static com.github.msemys.esjc.util.Strings.defaultIfEmpty; 7 | import static java.nio.charset.StandardCharsets.UTF_8; 8 | 9 | /** 10 | * Exception thrown if a user operation fails. 11 | */ 12 | public class UserException extends RuntimeException { 13 | 14 | /** 15 | * The HTTP status code returned by the server 16 | */ 17 | public final int httpStatusCode; 18 | 19 | /** 20 | * Creates a new instance with the specified status code and error message. 21 | * 22 | * @param httpStatusCode HTTP status code. 23 | * @param message error message. 24 | */ 25 | public UserException(int httpStatusCode, String message) { 26 | super(message); 27 | this.httpStatusCode = httpStatusCode; 28 | } 29 | 30 | /** 31 | * Creates a new instance from the specified HTTP request and response. 32 | * 33 | * @param request HTTP request. 34 | * @param response HTTP response. 35 | */ 36 | public UserException(HttpRequest request, FullHttpResponse response) { 37 | this(response.status().code(), String.format("Server returned %d (%s) for %s on %s", 38 | response.status().code(), 39 | defaultIfEmpty(response.content().toString(UTF_8), response.status().reasonPhrase()), 40 | request.method().name(), 41 | request.uri())); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/EventReadResult.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.github.msemys.esjc.proto.EventStoreClientMessages.ResolvedIndexedEvent; 4 | 5 | import static com.github.msemys.esjc.util.Preconditions.checkArgument; 6 | import static com.github.msemys.esjc.util.Strings.isNullOrEmpty; 7 | 8 | /** 9 | * Result type of a single read operation to the Event Store that retrieves one event. 10 | */ 11 | public class EventReadResult { 12 | 13 | /** 14 | * Status of read attempt. 15 | */ 16 | public final EventReadStatus status; 17 | 18 | /** 19 | * The name of the stream read. 20 | */ 21 | public final String stream; 22 | 23 | /** 24 | * The event number of the requested event. 25 | */ 26 | public final long eventNumber; 27 | 28 | /** 29 | * The event read. 30 | */ 31 | public final ResolvedEvent event; 32 | 33 | /** 34 | * Creates a new instance. 35 | * 36 | * @param status status of read attempt. 37 | * @param stream the name of the stream read. 38 | * @param eventNumber the event number. 39 | * @param event the event read. 40 | */ 41 | public EventReadResult(EventReadStatus status, String stream, long eventNumber, ResolvedIndexedEvent event) { 42 | checkArgument(!isNullOrEmpty(stream), "stream is null or empty"); 43 | this.status = status; 44 | this.stream = stream; 45 | this.eventNumber = eventNumber; 46 | this.event = (status == EventReadStatus.Success) ? new ResolvedEvent(event) : null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/util/Preconditions.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.util; 2 | 3 | public class Preconditions { 4 | 5 | public static T checkNotNull(T reference, String errorMessage) { 6 | if (reference == null) { 7 | throw new NullPointerException(errorMessage); 8 | } 9 | return reference; 10 | } 11 | 12 | public static T checkNotNull(T reference, String errorMessage, Object... errorMessageArgs) { 13 | if (reference == null) { 14 | throw new NullPointerException(String.format(errorMessage, errorMessageArgs)); 15 | } 16 | return reference; 17 | } 18 | 19 | public static void checkArgument(boolean expression, String errorMessage) { 20 | if (!expression) { 21 | throw new IllegalArgumentException(errorMessage); 22 | } 23 | } 24 | 25 | public static void checkArgument(boolean expression, String errorMessage, Object... errorMessageArgs) { 26 | if (!expression) { 27 | throw new IllegalArgumentException(String.format(errorMessage, errorMessageArgs)); 28 | } 29 | } 30 | 31 | public static void checkState(boolean expression, String errorMessage) { 32 | if (!expression) { 33 | throw new IllegalStateException(errorMessage); 34 | } 35 | } 36 | 37 | public static void checkState(boolean expression, String errorMessage, Object... errorMessageArgs) { 38 | if (!expression) { 39 | throw new IllegalStateException(String.format(errorMessage, errorMessageArgs)); 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/projection/ProjectionException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.projection; 2 | 3 | import io.netty.handler.codec.http.FullHttpResponse; 4 | import io.netty.handler.codec.http.HttpRequest; 5 | 6 | import static com.github.msemys.esjc.util.Strings.defaultIfEmpty; 7 | import static java.nio.charset.StandardCharsets.UTF_8; 8 | 9 | /** 10 | * Exception thrown if a projection operation fails. 11 | */ 12 | public class ProjectionException extends RuntimeException { 13 | 14 | /** 15 | * The HTTP status code returned by the server 16 | */ 17 | public final int httpStatusCode; 18 | 19 | /** 20 | * Creates a new instance with the specified status code and error message. 21 | * 22 | * @param httpStatusCode HTTP status code. 23 | * @param message error message. 24 | */ 25 | public ProjectionException(int httpStatusCode, String message) { 26 | super(message); 27 | this.httpStatusCode = httpStatusCode; 28 | } 29 | 30 | /** 31 | * Creates a new instance from the specified HTTP request and response. 32 | * 33 | * @param request HTTP request. 34 | * @param response HTTP response. 35 | */ 36 | public ProjectionException(HttpRequest request, FullHttpResponse response) { 37 | this(response.status().code(), String.format("Server returned %d (%s) for %s on %s", 38 | response.status().code(), 39 | defaultIfEmpty(response.content().toString(UTF_8), response.status().reasonPhrase()), 40 | request.method().name(), 41 | request.uri())); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/http/handler/HttpResponseHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.http.handler; 2 | 3 | import com.github.msemys.esjc.http.HttpClientException; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.SimpleChannelInboundHandler; 6 | import io.netty.handler.codec.http.FullHttpResponse; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import java.util.concurrent.CompletableFuture; 11 | 12 | public class HttpResponseHandler extends SimpleChannelInboundHandler { 13 | private static final Logger logger = LoggerFactory.getLogger(HttpResponseHandler.class); 14 | 15 | public volatile CompletableFuture pendingResponse; 16 | 17 | @Override 18 | protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception { 19 | if (pendingResponse != null) { 20 | pendingResponse.complete(msg); 21 | } else { 22 | logger.warn("Unexpected HTTP response received: {}", msg); 23 | } 24 | } 25 | 26 | @Override 27 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 28 | if (pendingResponse != null) { 29 | pendingResponse.completeExceptionally(cause); 30 | } 31 | } 32 | 33 | @Override 34 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 35 | if (pendingResponse != null) { 36 | pendingResponse.completeExceptionally(new HttpClientException("Connection closed")); 37 | } 38 | ctx.fireChannelInactive(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/StreamMetadataResult.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import static com.github.msemys.esjc.util.Preconditions.checkArgument; 4 | import static com.github.msemys.esjc.util.Strings.isNullOrEmpty; 5 | 6 | /** 7 | * Represents stream metadata as a series of properties for system data and 8 | * a {@link StreamMetadata} object for user metadata. 9 | */ 10 | public class StreamMetadataResult { 11 | 12 | /** 13 | * The name of the stream. 14 | */ 15 | public final String stream; 16 | 17 | /** 18 | * Indicates whether or not the stream is deleted. 19 | */ 20 | public final boolean isStreamDeleted; 21 | 22 | /** 23 | * The version of the metadata format. 24 | */ 25 | public final long metastreamVersion; 26 | 27 | /** 28 | * User-specified stream metadata. 29 | */ 30 | public final StreamMetadata streamMetadata; 31 | 32 | /** 33 | * Creates a new instance. 34 | * 35 | * @param stream the name of the stream. 36 | * @param isStreamDeleted whether the stream is soft-deleted. 37 | * @param metastreamVersion the version of the metadata format. 38 | * @param streamMetadata user-specified stream metadata. 39 | */ 40 | public StreamMetadataResult(String stream, boolean isStreamDeleted, long metastreamVersion, StreamMetadata streamMetadata) { 41 | checkArgument(!isNullOrEmpty(stream), "stream is null or empty"); 42 | this.stream = stream; 43 | this.isStreamDeleted = isStreamDeleted; 44 | this.metastreamVersion = metastreamVersion; 45 | this.streamMetadata = streamMetadata; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/RawStreamMetadataResult.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import static com.github.msemys.esjc.util.Preconditions.checkArgument; 4 | import static com.github.msemys.esjc.util.Strings.isNullOrEmpty; 5 | 6 | /** 7 | * Represents stream metadata as a series of properties for system data and a byte array for user metadata. 8 | */ 9 | public class RawStreamMetadataResult { 10 | 11 | /** 12 | * The name of the stream. 13 | */ 14 | public final String stream; 15 | 16 | /** 17 | * Indicates whether or not the stream is soft-deleted. 18 | */ 19 | public final boolean isStreamDeleted; 20 | 21 | /** 22 | * The version of the metadata format. 23 | */ 24 | public final long metastreamVersion; 25 | 26 | /** 27 | * A byte array containing user-specified metadata. 28 | */ 29 | public final byte[] streamMetadata; 30 | 31 | /** 32 | * Creates a new instance. 33 | * 34 | * @param stream the name of the stream. 35 | * @param isStreamDeleted whether the stream is soft-deleted. 36 | * @param metastreamVersion the version of the metadata format. 37 | * @param streamMetadata a byte array containing user-specified metadata. 38 | */ 39 | public RawStreamMetadataResult(String stream, boolean isStreamDeleted, long metastreamVersion, byte[] streamMetadata) { 40 | checkArgument(!isNullOrEmpty(stream), "stream is null or empty"); 41 | this.stream = stream; 42 | this.isStreamDeleted = isStreamDeleted; 43 | this.metastreamVersion = metastreamVersion; 44 | this.streamMetadata = streamMetadata; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/Subscription.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import static com.github.msemys.esjc.util.Strings.isNullOrEmpty; 4 | 5 | /** 6 | * Subscription to a single stream or to the stream of all events in the Event Store. 7 | */ 8 | public abstract class Subscription implements AutoCloseable { 9 | 10 | /** 11 | * The name of the stream to which the subscription is subscribed. 12 | */ 13 | public final String streamId; 14 | 15 | /** 16 | * The last commit position seen on the subscription (if this is a subscription to all events). 17 | */ 18 | public final long lastCommitPosition; 19 | 20 | /** 21 | * The last event number seen on the subscription (if this is a subscription to a single stream). 22 | */ 23 | public final Long lastEventNumber; 24 | 25 | public Subscription(String streamId, long lastCommitPosition, Long lastEventNumber) { 26 | this.streamId = streamId; 27 | this.lastCommitPosition = lastCommitPosition; 28 | this.lastEventNumber = lastEventNumber; 29 | } 30 | 31 | /** 32 | * Determines whether or not this subscription is to $all stream or to a specific stream. 33 | * 34 | * @return {@code true} if this subscription is to $all stream, otherwise {@code false} 35 | */ 36 | public boolean isSubscribedToAll() { 37 | return isNullOrEmpty(streamId); 38 | } 39 | 40 | /** 41 | * Unsubscribes from the stream. 42 | */ 43 | public abstract void unsubscribe(); 44 | 45 | /** 46 | * Unsubscribes from the stream. 47 | */ 48 | @Override 49 | public void close() { 50 | unsubscribe(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/StreamEventsIterator.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.github.msemys.esjc.operation.StreamDeletedException; 4 | import com.github.msemys.esjc.operation.StreamNotFoundException; 5 | 6 | import java.util.List; 7 | import java.util.concurrent.CompletableFuture; 8 | import java.util.function.Function; 9 | 10 | /** 11 | * Stream events iterator. 12 | */ 13 | public class StreamEventsIterator extends AbstractEventsIterator { 14 | 15 | StreamEventsIterator(long eventNumber, Function> reader) { 16 | super(eventNumber, reader); 17 | } 18 | 19 | @Override 20 | protected void onBatchReceived(StreamEventsSlice slice) { 21 | switch (slice.status) { 22 | case Success: 23 | super.onBatchReceived(slice); 24 | break; 25 | case StreamNotFound: 26 | throw new StreamNotFoundException(slice.stream); 27 | case StreamDeleted: 28 | throw new StreamDeletedException(slice.stream); 29 | default: 30 | throw new IllegalStateException(String.format("Unexpected read status: %s", slice.status)); 31 | } 32 | } 33 | 34 | @Override 35 | protected Long getNextCursor(StreamEventsSlice slice) { 36 | return slice.nextEventNumber; 37 | } 38 | 39 | @Override 40 | protected List getEvents(StreamEventsSlice slice) { 41 | return slice.events; 42 | } 43 | 44 | @Override 45 | protected boolean isEndOfStream(StreamEventsSlice slice) { 46 | return slice.isEndOfStream; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/StreamEventsSpliterator.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.github.msemys.esjc.operation.StreamDeletedException; 4 | import com.github.msemys.esjc.operation.StreamNotFoundException; 5 | 6 | import java.util.List; 7 | import java.util.concurrent.CompletableFuture; 8 | import java.util.function.Function; 9 | 10 | /** 11 | * Stream events spliterator. 12 | */ 13 | public class StreamEventsSpliterator extends AbstractEventsSpliterator { 14 | 15 | StreamEventsSpliterator(long eventNumber, Function> reader) { 16 | super(eventNumber, reader); 17 | } 18 | 19 | @Override 20 | protected void onBatchReceived(StreamEventsSlice slice) { 21 | switch (slice.status) { 22 | case Success: 23 | super.onBatchReceived(slice); 24 | break; 25 | case StreamNotFound: 26 | throw new StreamNotFoundException(slice.stream); 27 | case StreamDeleted: 28 | throw new StreamDeletedException(slice.stream); 29 | default: 30 | throw new IllegalStateException(String.format("Unexpected read status: %s", slice.status)); 31 | } 32 | } 33 | 34 | @Override 35 | protected Long getNextCursor(StreamEventsSlice slice) { 36 | return slice.nextEventNumber; 37 | } 38 | 39 | @Override 40 | protected List getEvents(StreamEventsSlice slice) { 41 | return slice.events; 42 | } 43 | 44 | @Override 45 | protected boolean isEndOfStream(StreamEventsSlice slice) { 46 | return slice.isEndOfStream; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/system/SystemProjections.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.system; 2 | 3 | /** 4 | * System built-in projections. 5 | *

6 | * Note: when you start Event Store from a fresh database, these projections are present but disabled. 7 | */ 8 | public class SystemProjections { 9 | 10 | /** 11 | * Projection that links existing events from streams to a new stream with a {@code $ce-} prefix (a category) 12 | * by splitting a stream id by a configurable separator. 13 | *

14 | * Note: edit this projection to provide your own values, such as where to split the stream id and separator. 15 | */ 16 | public static final String BY_CATEGORY = "$by_category"; 17 | 18 | /** 19 | * Projection that links existing events from streams to a new stream with a {@code $et-} prefix. 20 | *

21 | * Note: this projection cannot be configured. 22 | */ 23 | public static final String BY_EVENT_TYPE = "$by_event_type"; 24 | 25 | /** 26 | * Projection that links existing events from streams to a new stream with a {@code $category-} prefix 27 | * by splitting a stream id by a configurable separator. 28 | *

29 | * Note: edit this projection to provide your own values, such as where to split the stream id and separator. 30 | */ 31 | public static final String STREAM_BY_CATEGORY = "$stream_by_category"; 32 | 33 | /** 34 | * Projection that links existing events from streams to a stream named $streams. 35 | *

36 | * Note: this projection cannot be configured. 37 | */ 38 | public static final String STREAMS = "$streams"; 39 | 40 | private SystemProjections() { 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/AbstractEventsIterator.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import java.util.Iterator; 4 | import java.util.List; 5 | import java.util.concurrent.CompletableFuture; 6 | import java.util.function.Function; 7 | 8 | import static java.util.Collections.emptyIterator; 9 | 10 | abstract class AbstractEventsIterator implements Iterator { 11 | private final Function> reader; 12 | private T cursor; 13 | private Iterator iterator; 14 | private boolean endOfStream; 15 | 16 | AbstractEventsIterator(T cursor, Function> reader) { 17 | this.cursor = cursor; 18 | this.reader = reader; 19 | } 20 | 21 | @Override 22 | public boolean hasNext() { 23 | return iterator().hasNext(); 24 | } 25 | 26 | @Override 27 | public ResolvedEvent next() { 28 | ResolvedEvent event = iterator().next(); 29 | 30 | if (!iterator.hasNext()) { 31 | iterator = endOfStream ? emptyIterator() : null; 32 | } 33 | 34 | return event; 35 | } 36 | 37 | private Iterator iterator() { 38 | if (iterator == null) { 39 | onBatchReceived(reader.apply(cursor).join()); 40 | } 41 | return iterator; 42 | } 43 | 44 | protected void onBatchReceived(R slice) { 45 | cursor = getNextCursor(slice); 46 | iterator = getEvents(slice).iterator(); 47 | endOfStream = isEndOfStream(slice); 48 | } 49 | 50 | protected abstract T getNextCursor(R slice); 51 | 52 | protected abstract List getEvents(R slice); 53 | 54 | protected abstract boolean isEndOfStream(R slice); 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/ITEventStoreListener.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.github.msemys.esjc.event.ClientConnected; 4 | import com.github.msemys.esjc.event.ClientDisconnected; 5 | import com.github.msemys.esjc.event.ConnectionClosed; 6 | import org.junit.Test; 7 | 8 | import java.util.concurrent.CountDownLatch; 9 | 10 | import static java.util.concurrent.TimeUnit.SECONDS; 11 | import static org.junit.Assert.assertTrue; 12 | 13 | public class ITEventStoreListener extends AbstractEventStoreTest { 14 | 15 | public ITEventStoreListener(EventStore eventstore) { 16 | super(eventstore); 17 | } 18 | 19 | @Test 20 | public void firesClientInternalEvents() throws InterruptedException { 21 | CountDownLatch clientConnectedSignal = new CountDownLatch(1); 22 | CountDownLatch clientDisconnectedSignal = new CountDownLatch(1); 23 | CountDownLatch connectionClosedSignal = new CountDownLatch(1); 24 | 25 | eventstore.addListener(event -> { 26 | if (event instanceof ClientConnected) { 27 | clientConnectedSignal.countDown(); 28 | } else if (event instanceof ClientDisconnected) { 29 | clientDisconnectedSignal.countDown(); 30 | } else if (event instanceof ConnectionClosed) { 31 | connectionClosedSignal.countDown(); 32 | } 33 | }); 34 | 35 | eventstore.connect(); 36 | 37 | assertTrue("client connect timeout", clientConnectedSignal.await(15, SECONDS)); 38 | 39 | eventstore.disconnect(); 40 | 41 | assertTrue("connection close timeout", connectionClosedSignal.await(2, SECONDS)); 42 | assertTrue("client disconnect timeout", clientDisconnectedSignal.await(2, SECONDS)); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/task/StartSubscription.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.task; 2 | 3 | import com.github.msemys.esjc.Subscription; 4 | import com.github.msemys.esjc.UserCredentials; 5 | import com.github.msemys.esjc.VolatileSubscriptionListener; 6 | 7 | import java.time.Duration; 8 | import java.util.concurrent.CompletableFuture; 9 | 10 | import static com.github.msemys.esjc.util.Preconditions.checkNotNull; 11 | 12 | public class StartSubscription implements Task { 13 | public final CompletableFuture result; 14 | 15 | public final String streamId; 16 | public final boolean resolveLinkTos; 17 | public final UserCredentials userCredentials; 18 | public final VolatileSubscriptionListener listener; 19 | 20 | public final int maxRetries; 21 | public final Duration timeout; 22 | 23 | public StartSubscription(CompletableFuture result, 24 | String streamId, 25 | boolean resolveLinkTos, 26 | UserCredentials userCredentials, 27 | VolatileSubscriptionListener listener, 28 | int maxRetries, 29 | Duration timeout) { 30 | checkNotNull(result, "result is null"); 31 | checkNotNull(listener, "listener is null"); 32 | 33 | this.result = result; 34 | this.streamId = streamId; 35 | this.resolveLinkTos = resolveLinkTos; 36 | this.userCredentials = userCredentials; 37 | this.listener = listener; 38 | this.maxRetries = maxRetries; 39 | this.timeout = timeout; 40 | } 41 | 42 | @Override 43 | public void fail(Exception exception) { 44 | result.completeExceptionally(exception); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/matcher/RecordedEventMatcher.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.matcher; 2 | 3 | import com.github.msemys.esjc.EventData; 4 | import com.github.msemys.esjc.RecordedEvent; 5 | import org.hamcrest.Description; 6 | import org.hamcrest.Factory; 7 | import org.hamcrest.Matcher; 8 | import org.hamcrest.TypeSafeMatcher; 9 | 10 | import static com.github.msemys.esjc.util.Strings.newString; 11 | 12 | public class RecordedEventMatcher extends TypeSafeMatcher { 13 | 14 | private final EventData expected; 15 | 16 | public RecordedEventMatcher(EventData expected) { 17 | this.expected = expected; 18 | } 19 | 20 | @Override 21 | protected boolean matchesSafely(RecordedEvent actual) { 22 | if (!expected.eventId.equals(actual.eventId)) { 23 | return false; 24 | } 25 | 26 | if (!expected.type.equals(actual.eventType)) { 27 | return false; 28 | } 29 | 30 | String expectedDataString = newString(expected.data); 31 | String expectedMetadataString = newString(expected.metadata); 32 | 33 | String actualDataString = newString(actual.data); 34 | String actualMetadataDataString = newString(actual.metadata); 35 | 36 | return expectedDataString.equals(actualDataString) && expectedMetadataString.equals(actualMetadataDataString); 37 | } 38 | 39 | @Override 40 | public void describeTo(Description description) { 41 | description.appendText("event ") 42 | .appendValue(expected.type) 43 | .appendText(" ") 44 | .appendValue(expected.eventId); 45 | } 46 | 47 | @Factory 48 | public static Matcher equalTo(EventData item) { 49 | return new RecordedEventMatcher(item); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/ExpectedVersion.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Expected version constants. 5 | *

6 | * The use of expected version can be a bit tricky especially when discussing idempotency assurances 7 | * given by the Event Store. There are three possible constant values that can be used for the 8 | * passing of an expected version: 9 | *

    10 | *
  • {@link ExpectedVersion#NO_STREAM} - says that the stream should not exist when doing your write.
  • 11 | *
  • {@link ExpectedVersion#ANY} - says that you should not conflict with anything.
  • 12 | *
  • {@link ExpectedVersion#STREAM_EXISTS} - says the stream or a metadata stream should exist when doing your write.
  • 13 | *
14 | * Any other value states that the last event written to the stream should have a sequence number 15 | * matching your expected value. 16 | *

17 | * The Event Store will assure idempotency for all operations using any value, except for 18 | * {@link ExpectedVersion#ANY} and {@link ExpectedVersion#STREAM_EXISTS}. When using {@link ExpectedVersion#ANY} or 19 | * {@link ExpectedVersion#STREAM_EXISTS} the Event Store will do its best to assure idempotency but 20 | * will not guarantee idempotency. 21 | */ 22 | public class ExpectedVersion { 23 | 24 | /** 25 | * Specifies the expectation that target stream does not yet exist. 26 | */ 27 | public static final long NO_STREAM = -1; 28 | 29 | /** 30 | * Disables the optimistic concurrency check (idempotence is not guaranteed). 31 | */ 32 | public static final long ANY = -2; 33 | 34 | /** 35 | * Specifies the expectation that stream or a metadata stream should exist (idempotence is not guaranteed). 36 | */ 37 | public static final long STREAM_EXISTS = -4; 38 | 39 | private ExpectedVersion() { 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/SubscriptionDropReason.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | /** 4 | * Represents the reason subscription drop happened. 5 | */ 6 | public enum SubscriptionDropReason { 7 | 8 | /** 9 | * Subscription dropped because the client called close. 10 | */ 11 | UserInitiated, 12 | 13 | /** 14 | * Subscription dropped because the client is not authenticated. 15 | */ 16 | NotAuthenticated, 17 | 18 | /** 19 | * Subscription dropped because access to the stream was denied. 20 | */ 21 | AccessDenied, 22 | 23 | /** 24 | * Subscription dropped because of an error in the subscription phase. 25 | */ 26 | SubscribingError, 27 | 28 | /** 29 | * Subscription dropped because of a server error. 30 | */ 31 | ServerError, 32 | 33 | /** 34 | * Subscription dropped because the connection was closed. 35 | */ 36 | ConnectionClosed, 37 | 38 | /** 39 | * Subscription dropped because of an error during the catch-up phase. 40 | */ 41 | CatchUpError, 42 | 43 | /** 44 | * Subscription dropped because it's queue overflowed. 45 | */ 46 | ProcessingQueueOverflow, 47 | 48 | /** 49 | * Subscription dropped because an exception was thrown by a handler. 50 | */ 51 | EventHandlerException, 52 | 53 | /** 54 | * The maximum number of subscribers for the persistent subscription has been reached. 55 | */ 56 | MaxSubscribersReached, 57 | 58 | /** 59 | * The persistent subscription has been deleted. 60 | */ 61 | PersistentSubscriptionDeleted, 62 | 63 | /** 64 | * Subscription was dropped for an unknown reason. 65 | */ 66 | Unknown, 67 | 68 | /** 69 | * Target of persistent subscription was not found. Needs to be created first. 70 | */ 71 | NotFound 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/node/cluster/GossipSeed.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.node.cluster; 2 | 3 | import java.net.InetSocketAddress; 4 | 5 | /** 6 | * Represents a source of cluster gossip. 7 | */ 8 | public class GossipSeed { 9 | 10 | /** 11 | * The endpoint for the External HTTP endpoint of the gossip seed. 12 | *

13 | * The HTTP endpoint is used rather than the TCP endpoint because it is required 14 | * for the client to exchange gossip with the server. The standard port which should be 15 | * used here is 2113. 16 | *

17 | */ 18 | public final InetSocketAddress endpoint; 19 | 20 | /** 21 | * The host header to be sent when requesting gossip. 22 | */ 23 | public final String hostHeader; 24 | 25 | /** 26 | * Creates a new instance with the specified endpoint and empty host header. 27 | * 28 | * @param endpoint the endpoint for the External HTTP endpoint of the gossip seed. 29 | */ 30 | public GossipSeed(InetSocketAddress endpoint) { 31 | this(endpoint, ""); 32 | } 33 | 34 | /** 35 | * Creates a new instance with the specified endpoint and host header. 36 | * 37 | * @param endpoint the endpoint for the External HTTP endpoint of the gossip seed. 38 | * @param hostHeader the host header to be sent when requesting gossip. 39 | */ 40 | public GossipSeed(InetSocketAddress endpoint, String hostHeader) { 41 | this.endpoint = endpoint; 42 | this.hostHeader = hostHeader; 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | final StringBuilder sb = new StringBuilder("GossipSeed{"); 48 | sb.append("endpoint=").append(endpoint); 49 | sb.append(", hostHeader='").append(hostHeader).append('\''); 50 | sb.append('}'); 51 | return sb.toString(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/subscription/manager/SubscriptionItem.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.subscription.manager; 2 | 3 | import com.github.msemys.esjc.subscription.SubscriptionOperation; 4 | import com.github.msemys.esjc.util.SystemTime; 5 | import io.netty.channel.ChannelId; 6 | 7 | import java.time.Duration; 8 | import java.time.Instant; 9 | import java.util.UUID; 10 | 11 | import static com.github.msemys.esjc.util.Preconditions.checkNotNull; 12 | 13 | public class SubscriptionItem { 14 | public final SubscriptionOperation operation; 15 | public final int maxRetries; 16 | public final Duration timeout; 17 | public final Instant createdTime; 18 | 19 | public ChannelId connectionId; 20 | public UUID correlationId; 21 | public boolean isSubscribed; 22 | public int retryCount; 23 | public final SystemTime lastUpdated; 24 | 25 | public SubscriptionItem(SubscriptionOperation operation, int maxRetries, Duration timeout) { 26 | checkNotNull(operation, "operation is null"); 27 | 28 | this.operation = operation; 29 | this.maxRetries = maxRetries; 30 | this.timeout = timeout; 31 | this.createdTime = Instant.now(); 32 | 33 | correlationId = UUID.randomUUID(); 34 | retryCount = 0; 35 | lastUpdated = SystemTime.now(); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return new StringBuilder() 41 | .append("Subscription ").append(operation.getClass().getSimpleName()) 42 | .append(" (").append(correlationId).append("): ").append(operation) 43 | .append(", is subscribed: ").append(isSubscribed) 44 | .append(", retry count: ").append(retryCount) 45 | .append(", created: ").append(createdTime) 46 | .append(", last updated: ").append(lastUpdated) 47 | .toString(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/AllEventsSlice.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.github.msemys.esjc.proto.EventStoreClientMessages; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import static java.util.Collections.emptyList; 9 | import static java.util.stream.Collectors.toCollection; 10 | 11 | /** 12 | * Result type of a single read operation to the Event Store, that retrieves event list from $all stream. 13 | */ 14 | public class AllEventsSlice { 15 | 16 | /** 17 | * The direction of read request. 18 | */ 19 | public final ReadDirection readDirection; 20 | 21 | /** 22 | * The position where this slice was read from. 23 | */ 24 | public final Position fromPosition; 25 | 26 | /** 27 | * The position where the next slice should be read from. 28 | */ 29 | public final Position nextPosition; 30 | 31 | /** 32 | * The events read. 33 | */ 34 | public final List events; 35 | 36 | public AllEventsSlice(ReadDirection readDirection, 37 | Position fromPosition, 38 | Position nextPosition, 39 | List events) { 40 | this.readDirection = readDirection; 41 | this.fromPosition = fromPosition; 42 | this.nextPosition = nextPosition; 43 | this.events = (events == null) ? emptyList() : events.stream() 44 | .map(ResolvedEvent::new) 45 | .collect(toCollection(() -> new ArrayList<>(events.size()))); 46 | } 47 | 48 | /** 49 | * Determines whether or not this is the end of the $all stream. 50 | * 51 | * @return {@code true} if the $all stream is ended, otherwise {@code false} 52 | */ 53 | public boolean isEndOfStream() { 54 | return events.isEmpty(); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/ITTryAppendToStream.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class ITTryAppendToStream extends AbstractEventStoreTest { 8 | 9 | public ITTryAppendToStream(EventStore eventstore) { 10 | super(eventstore); 11 | } 12 | 13 | @Test 14 | public void returnsWrongExpectedVersionStatusWhenAttemptsToWriteWithInvalidExpectedVersion() { 15 | final String stream = generateStreamName(); 16 | 17 | WriteAttemptResult result = eventstore.tryAppendToStream(stream, 17, newTestEvent()).join(); 18 | 19 | assertEquals(WriteStatus.WrongExpectedVersion, result.status); 20 | assertEquals(ExpectedVersion.ANY, result.nextExpectedVersion); 21 | assertNull(result.logPosition); 22 | } 23 | 24 | @Test 25 | public void returnsStreamDeletedStatusWhenAttemptsToWriteToDeletedStream() { 26 | final String stream = generateStreamName(); 27 | 28 | eventstore.appendToStream(stream, ExpectedVersion.ANY, newTestEvent()).join(); 29 | eventstore.deleteStream(stream, ExpectedVersion.ANY, true).join(); 30 | 31 | WriteAttemptResult result = eventstore.tryAppendToStream(stream, ExpectedVersion.ANY, newTestEvent()).join(); 32 | 33 | assertEquals(WriteStatus.StreamDeleted, result.status); 34 | assertEquals(ExpectedVersion.ANY, result.nextExpectedVersion); 35 | assertNull(result.logPosition); 36 | } 37 | 38 | @Test 39 | public void returnsSuccessStatusWhenAttemptsToWriteWithCorrectExpectedVersion() { 40 | final String stream = generateStreamName(); 41 | 42 | WriteAttemptResult result = eventstore.tryAppendToStream(stream, ExpectedVersion.NO_STREAM, newTestEvent()).join(); 43 | 44 | assertEquals(WriteStatus.Success, result.status); 45 | assertEquals(0, result.nextExpectedVersion); 46 | assertNotNull(result.logPosition); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/ITReadAllEventsForwardWithLinkToPassedMaxCount.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | public class ITReadAllEventsForwardWithLinkToPassedMaxCount extends AbstractEventStoreTest { 8 | 9 | public ITReadAllEventsForwardWithLinkToPassedMaxCount(EventStore eventstore) { 10 | super(eventstore); 11 | } 12 | 13 | @Test 14 | public void readsOneEvent() { 15 | final String deletedStreamName = generateStreamName(); 16 | final String linkedStreamName = generateStreamName(); 17 | 18 | eventstore.appendToStream(deletedStreamName, ExpectedVersion.ANY, 19 | EventData.newBuilder() 20 | .type("testing1") 21 | .jsonData("{'foo' : 4}") 22 | .build() 23 | ).join(); 24 | 25 | eventstore.setStreamMetadata(deletedStreamName, ExpectedVersion.ANY, 26 | StreamMetadata.newBuilder() 27 | .maxCount(2L) 28 | .build() 29 | ).join(); 30 | 31 | eventstore.appendToStream(deletedStreamName, ExpectedVersion.ANY, 32 | EventData.newBuilder() 33 | .type("testing2") 34 | .jsonData("{'foo' : 4}") 35 | .build() 36 | ).join(); 37 | 38 | eventstore.appendToStream(deletedStreamName, ExpectedVersion.ANY, 39 | EventData.newBuilder() 40 | .type("testing3") 41 | .jsonData("{'foo' : 4}") 42 | .build() 43 | ).join(); 44 | 45 | eventstore.appendToStream(linkedStreamName, ExpectedVersion.ANY, 46 | EventData.newBuilder() 47 | .linkTo(0, deletedStreamName) 48 | .build() 49 | ).join(); 50 | 51 | StreamEventsSlice slice = eventstore.readStreamEventsForward(linkedStreamName, 0, 1, true).join(); 52 | 53 | assertEquals(1, slice.events.size()); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/user/User.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.user; 2 | 3 | import java.time.OffsetDateTime; 4 | import java.util.List; 5 | 6 | /** 7 | * Represents the details for a user. 8 | */ 9 | public class User { 10 | 11 | /** 12 | * The login name of the user. 13 | */ 14 | public final String loginName; 15 | 16 | /** 17 | * The full name of the user. 18 | */ 19 | public final String fullName; 20 | 21 | /** 22 | * The groups the user is a member of. 23 | */ 24 | public final List groups; 25 | 26 | /** 27 | * The date/time the user was updated in UTC format. 28 | */ 29 | public final OffsetDateTime dateLastUpdated; 30 | 31 | /** 32 | * Whether or not the user is disabled. 33 | */ 34 | public final boolean disabled; 35 | 36 | /** 37 | * List of hypermedia links describing actions allowed on user resource. 38 | */ 39 | public final List links; 40 | 41 | public User(String loginName, 42 | String fullName, 43 | List groups, 44 | OffsetDateTime dateLastUpdated, 45 | boolean disabled, 46 | List links) { 47 | this.loginName = loginName; 48 | this.fullName = fullName; 49 | this.groups = groups; 50 | this.dateLastUpdated = dateLastUpdated; 51 | this.disabled = disabled; 52 | this.links = links; 53 | } 54 | 55 | @Override 56 | public String toString() { 57 | final StringBuilder sb = new StringBuilder("User{"); 58 | sb.append("loginName='").append(loginName).append('\''); 59 | sb.append(", fullName='").append(fullName).append('\''); 60 | sb.append(", groups=").append(groups); 61 | sb.append(", dateLastUpdated=").append(dateLastUpdated); 62 | sb.append(", disabled=").append(disabled); 63 | sb.append(", links=").append(links); 64 | sb.append('}'); 65 | return sb.toString(); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/rule/RetryableMethodRule.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.rule; 2 | 3 | import org.junit.rules.MethodRule; 4 | import org.junit.runners.model.FrameworkMethod; 5 | import org.junit.runners.model.Statement; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import static com.github.msemys.esjc.util.Threads.sleepUninterruptibly; 10 | 11 | public class RetryableMethodRule implements MethodRule { 12 | private static final Logger logger = LoggerFactory.getLogger(RetryableMethodRule.class); 13 | 14 | @Override 15 | public Statement apply(Statement base, FrameworkMethod method, Object target) { 16 | return new Statement() { 17 | 18 | @Override 19 | public void evaluate() throws Throwable { 20 | Retryable retryable = method.getAnnotation(Retryable.class); 21 | 22 | if (retryable != null) { 23 | for (int i = 1; i <= retryable.maxAttempts(); i++) { 24 | try { 25 | base.evaluate(); 26 | break; 27 | } catch (Throwable t) { 28 | if (retryable.value().isAssignableFrom(t.getClass())) { 29 | logger.warn("Attempt {}/{} failed.", i, retryable.maxAttempts(), t); 30 | 31 | if (i == retryable.maxAttempts()) { 32 | throw t; 33 | } else { 34 | if (retryable.delay() > 0) { 35 | sleepUninterruptibly(retryable.delay()); 36 | } 37 | } 38 | } else { 39 | throw t; 40 | } 41 | } 42 | } 43 | } else { 44 | base.evaluate(); 45 | } 46 | } 47 | }; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/SystemSettingsJsonAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.google.gson.TypeAdapter; 4 | import com.google.gson.stream.JsonReader; 5 | import com.google.gson.stream.JsonToken; 6 | import com.google.gson.stream.JsonWriter; 7 | 8 | import java.io.IOException; 9 | 10 | public class SystemSettingsJsonAdapter extends TypeAdapter { 11 | private static final String USER_STREAM_ACL = "$userStreamAcl"; 12 | private static final String SYSTEM_STREAM_ACL = "$systemStreamAcl"; 13 | 14 | private final StreamAclJsonAdapter streamAclJsonAdapter = new StreamAclJsonAdapter(); 15 | 16 | @Override 17 | public void write(JsonWriter writer, SystemSettings value) throws IOException { 18 | writer.beginObject(); 19 | 20 | if (value.userStreamAcl != null) { 21 | writer.name(USER_STREAM_ACL); 22 | streamAclJsonAdapter.write(writer, value.userStreamAcl); 23 | } 24 | 25 | if (value.systemStreamAcl != null) { 26 | writer.name(SYSTEM_STREAM_ACL); 27 | streamAclJsonAdapter.write(writer, value.systemStreamAcl); 28 | } 29 | 30 | writer.endObject(); 31 | } 32 | 33 | @Override 34 | public SystemSettings read(JsonReader reader) throws IOException { 35 | if (reader.peek() == JsonToken.NULL) { 36 | return null; 37 | } 38 | 39 | SystemSettings.Builder builder = SystemSettings.newBuilder(); 40 | 41 | reader.beginObject(); 42 | 43 | while (reader.peek() != JsonToken.END_OBJECT && reader.hasNext()) { 44 | String name = reader.nextName(); 45 | switch (name) { 46 | case USER_STREAM_ACL: 47 | builder.userStreamAcl(streamAclJsonAdapter.read(reader)); 48 | break; 49 | case SYSTEM_STREAM_ACL: 50 | builder.systemStreamAcl(streamAclJsonAdapter.read(reader)); 51 | break; 52 | } 53 | } 54 | 55 | reader.endObject(); 56 | 57 | return builder.build(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/util/Subscriptions.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.util; 2 | 3 | import com.github.msemys.esjc.ResolvedEvent; 4 | import com.github.msemys.esjc.RetryableResolvedEvent; 5 | import com.github.msemys.esjc.StreamPosition; 6 | import com.github.msemys.esjc.SubscriptionDropReason; 7 | import com.github.msemys.esjc.proto.EventStoreClientMessages.EventRecord; 8 | import com.google.protobuf.ByteString; 9 | 10 | import java.util.UUID; 11 | 12 | import static com.github.msemys.esjc.util.UUIDConverter.toBytes; 13 | 14 | public class Subscriptions { 15 | private static final EventRecord DUMMY_EVENT_RECORD = EventRecord.newBuilder() 16 | .setEventId(ByteString.copyFrom(toBytes(new UUID(0, 0)))) 17 | .setEventStreamId("dummy") 18 | .setEventNumber(StreamPosition.END) 19 | .setEventType("dummy") 20 | .setDataContentType(0) 21 | .setMetadataContentType(0) 22 | .setData(ByteString.EMPTY) 23 | .build(); 24 | 25 | public static final ResolvedEvent DROP_SUBSCRIPTION_EVENT = 26 | new ResolvedEvent(com.github.msemys.esjc.proto.EventStoreClientMessages.ResolvedEvent.newBuilder() 27 | .setEvent(DUMMY_EVENT_RECORD) 28 | .setCommitPosition(-1) 29 | .setPreparePosition(-1) 30 | .build()); 31 | 32 | public static final RetryableResolvedEvent DROP_PERSISTENT_SUBSCRIPTION_EVENT = new RetryableResolvedEvent(com.github.msemys.esjc.proto.EventStoreClientMessages.ResolvedIndexedEvent.newBuilder() 33 | .setEvent(DUMMY_EVENT_RECORD) 34 | .build(), 35 | -1); 36 | 37 | public static final DropData UNKNOWN_DROP_DATA = new DropData(SubscriptionDropReason.Unknown, new Exception("Drop reason not specified.")); 38 | 39 | private Subscriptions() { 40 | } 41 | 42 | public static class DropData { 43 | public final SubscriptionDropReason reason; 44 | public final Exception exception; 45 | 46 | public DropData(SubscriptionDropReason reason, Exception exception) { 47 | this.reason = reason; 48 | this.exception = exception; 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/ITReadAllEventsForwardWithHardDeletedStream.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.github.msemys.esjc.system.SystemEventTypes; 4 | import org.junit.Test; 5 | 6 | import java.util.List; 7 | 8 | import static com.github.msemys.esjc.matcher.RecordedEventListMatcher.containsInOrder; 9 | import static java.util.stream.Collectors.toList; 10 | import static org.junit.Assert.*; 11 | 12 | public class ITReadAllEventsForwardWithHardDeletedStream extends AbstractEventStoreTest { 13 | 14 | public ITReadAllEventsForwardWithHardDeletedStream(EventStore eventstore) { 15 | super(eventstore); 16 | } 17 | 18 | @Test 19 | public void ensuresDeletedStream() { 20 | final String stream = generateStreamName(); 21 | 22 | eventstore.appendToStream(stream, ExpectedVersion.NO_STREAM, newTestEvents(20)).join(); 23 | eventstore.deleteStream(stream, ExpectedVersion.ANY, true).join(); 24 | 25 | StreamEventsSlice slice = eventstore.readStreamEventsForward(stream, 0, 100, false).join(); 26 | 27 | assertEquals(SliceReadStatus.StreamDeleted, slice.status); 28 | assertTrue(slice.events.isEmpty()); 29 | } 30 | 31 | @Test 32 | public void returnsAllEventsIncludingTombstone() { 33 | final String stream = generateStreamName(); 34 | 35 | List events = newTestEvents(20); 36 | Position position = eventstore.appendToStream(stream, ExpectedVersion.NO_STREAM, events.get(0)).join().logPosition; 37 | eventstore.appendToStream(stream, 0, events.stream().skip(1).collect(toList())).join(); 38 | eventstore.deleteStream(stream, ExpectedVersion.ANY, true).join(); 39 | 40 | AllEventsSlice slice = eventstore.readAllEventsForward(position, events.size() + 10, false).join(); 41 | 42 | assertThat(slice.events.stream().limit(events.size()).map(e -> e.event).collect(toList()), containsInOrder(events)); 43 | 44 | RecordedEvent lastEvent = slice.events.get(slice.events.size() - 1).event; 45 | assertEquals(stream, lastEvent.eventStreamId); 46 | assertEquals(SystemEventTypes.STREAM_DELETED, lastEvent.eventType); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/AbstractSslConnectionTest.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.github.msemys.esjc.event.Event; 4 | import org.hamcrest.Matcher; 5 | import org.junit.Rule; 6 | import org.junit.rules.ExpectedException; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.concurrent.CountDownLatch; 11 | import java.util.function.Consumer; 12 | 13 | import static java.util.Arrays.asList; 14 | import static java.util.concurrent.TimeUnit.SECONDS; 15 | import static org.hamcrest.CoreMatchers.*; 16 | import static org.junit.Assert.assertThat; 17 | import static org.junit.Assert.assertTrue; 18 | 19 | public class AbstractSslConnectionTest { 20 | 21 | @Rule 22 | public ExpectedException expectedException = ExpectedException.none(); 23 | 24 | protected static void assertSignal(CountDownLatch signal) throws InterruptedException { 25 | assertTrue("timeout", signal.await(15, SECONDS)); 26 | } 27 | 28 | @SafeVarargs 29 | protected static void assertThrowable(Throwable throwable, Matcher... matchers) { 30 | assertThat(throwable, is(notNullValue())); 31 | assertThat(unwrapCauses(throwable), hasItem(allOf(asList(matchers)))); 32 | } 33 | 34 | @SuppressWarnings("unchecked") 35 | protected static EventStoreListener onEvent(Class eventClass, Consumer consumer) { 36 | return event -> { 37 | if (eventClass.isAssignableFrom(event.getClass())) { 38 | consumer.accept((T) event); 39 | } 40 | }; 41 | } 42 | 43 | protected static EventStoreBuilder newEventStoreBuilder() { 44 | return EventStoreBuilder.newBuilder() 45 | .singleNodeAddress("127.0.0.1", 7779) 46 | .userCredentials("admin", "changeit") 47 | .maxReconnections(2); 48 | } 49 | 50 | private static List unwrapCauses(Throwable thrown) { 51 | List result = new ArrayList<>(); 52 | 53 | do { 54 | result.add(thrown); 55 | thrown = thrown.getCause(); 56 | } while (thrown != null); 57 | 58 | return result; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/node/single/SingleNodeSettings.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.node.single; 2 | 3 | import java.net.InetSocketAddress; 4 | 5 | import static com.github.msemys.esjc.util.Preconditions.checkNotNull; 6 | 7 | /** 8 | * Single node settings. 9 | */ 10 | public class SingleNodeSettings { 11 | 12 | /** 13 | * Server address. 14 | */ 15 | public final InetSocketAddress address; 16 | 17 | private SingleNodeSettings(Builder builder) { 18 | address = builder.address; 19 | } 20 | 21 | /** 22 | * Creates a new single-node settings builder. 23 | * 24 | * @return single-node settings builder 25 | */ 26 | public static Builder newBuilder() { 27 | return new Builder(); 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | final StringBuilder sb = new StringBuilder("SingleNodeSettings{"); 33 | sb.append("address=").append(address); 34 | sb.append('}'); 35 | return sb.toString(); 36 | } 37 | 38 | /** 39 | * Single node settings builder. 40 | */ 41 | public static class Builder { 42 | private InetSocketAddress address; 43 | 44 | /** 45 | * Sets server address. 46 | * 47 | * @param host the host name. 48 | * @param port The port number. 49 | * @return the builder reference 50 | */ 51 | public Builder address(String host, int port) { 52 | return address(InetSocketAddress.createUnresolved(host, port)); 53 | } 54 | 55 | /** 56 | * Sets server address. 57 | * 58 | * @param address the server address. 59 | * @return the builder reference 60 | */ 61 | public Builder address(InetSocketAddress address) { 62 | this.address = address; 63 | return this; 64 | } 65 | 66 | /** 67 | * Builds a single-node settings. 68 | * 69 | * @return single-node settings 70 | */ 71 | public SingleNodeSettings build() { 72 | checkNotNull(address, "address is null"); 73 | return new SingleNodeSettings(this); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/task/StartPersistentSubscription.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.task; 2 | 3 | import com.github.msemys.esjc.RetryableResolvedEvent; 4 | import com.github.msemys.esjc.Subscription; 5 | import com.github.msemys.esjc.SubscriptionListener; 6 | import com.github.msemys.esjc.UserCredentials; 7 | import com.github.msemys.esjc.subscription.PersistentSubscriptionChannel; 8 | 9 | import java.time.Duration; 10 | import java.util.concurrent.CompletableFuture; 11 | 12 | import static com.github.msemys.esjc.util.Preconditions.checkNotNull; 13 | 14 | public class StartPersistentSubscription implements Task { 15 | public final CompletableFuture result; 16 | 17 | public final String subscriptionId; 18 | public final String streamId; 19 | public final int bufferSize; 20 | public final UserCredentials userCredentials; 21 | public final SubscriptionListener listener; 22 | public final int maxRetries; 23 | public final Duration timeout; 24 | 25 | public StartPersistentSubscription(CompletableFuture result, 26 | String subscriptionId, 27 | String streamId, 28 | int bufferSize, 29 | UserCredentials userCredentials, 30 | SubscriptionListener listener, 31 | int maxRetries, 32 | Duration timeout) { 33 | checkNotNull(result, "result is null"); 34 | checkNotNull(listener, "listener is null"); 35 | 36 | this.result = result; 37 | this.subscriptionId = subscriptionId; 38 | this.streamId = streamId; 39 | this.bufferSize = bufferSize; 40 | this.userCredentials = userCredentials; 41 | this.listener = listener; 42 | this.maxRetries = maxRetries; 43 | this.timeout = timeout; 44 | } 45 | 46 | @Override 47 | public void fail(Exception exception) { 48 | result.completeExceptionally(exception); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/operation/manager/OperationItem.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.operation.manager; 2 | 3 | import com.github.msemys.esjc.operation.Operation; 4 | import com.github.msemys.esjc.util.SystemTime; 5 | import io.netty.channel.ChannelId; 6 | 7 | import java.time.Duration; 8 | import java.time.Instant; 9 | import java.util.UUID; 10 | import java.util.concurrent.atomic.AtomicLong; 11 | 12 | import static com.github.msemys.esjc.util.Preconditions.checkNotNull; 13 | 14 | public class OperationItem implements Comparable { 15 | private static final AtomicLong sequencer = new AtomicLong(-1); 16 | 17 | public final long sequenceNo = sequencer.incrementAndGet(); 18 | 19 | public final Operation operation; 20 | public final int maxRetries; 21 | public final Duration timeout; 22 | public final Instant createdTime; 23 | 24 | public ChannelId connectionId; 25 | public UUID correlationId; 26 | public int retryCount; 27 | public final SystemTime lastUpdated; 28 | 29 | public OperationItem(Operation operation, int maxRetries, Duration timeout) { 30 | checkNotNull(operation, "operation is null"); 31 | 32 | this.operation = operation; 33 | this.maxRetries = maxRetries; 34 | this.timeout = timeout; 35 | this.createdTime = Instant.now(); 36 | 37 | correlationId = UUID.randomUUID(); 38 | retryCount = 0; 39 | lastUpdated = SystemTime.now(); 40 | } 41 | 42 | @Override 43 | public int compareTo(OperationItem o) { 44 | if (sequenceNo < o.sequenceNo) { 45 | return -1; 46 | } else if (sequenceNo > o.sequenceNo) { 47 | return 1; 48 | } else { 49 | return 0; 50 | } 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | final StringBuilder sb = new StringBuilder(); 56 | sb.append("Operation (").append(operation.getClass().getSimpleName()).append("): "); 57 | sb.append(correlationId).append(", retry count: ").append(retryCount).append(", "); 58 | sb.append("created: ").append(createdTime).append(", "); 59 | sb.append("last updated: ").append(lastUpdated); 60 | return sb.toString(); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/operation/WrongExpectedVersionException.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.operation; 2 | 3 | import com.github.msemys.esjc.EventStoreException; 4 | 5 | /** 6 | * Exception thrown if the expected version specified on an operation 7 | * does not match the version of the stream when the operation was attempted. 8 | */ 9 | public class WrongExpectedVersionException extends EventStoreException { 10 | public final String stream; 11 | public final Long expectedVersion; 12 | public final Long currentVersion; 13 | 14 | /** 15 | * Creates a new instance with the specified error message. 16 | * 17 | * @param message error message. 18 | */ 19 | public WrongExpectedVersionException(String message) { 20 | super(message); 21 | this.stream = null; 22 | this.expectedVersion = null; 23 | this.currentVersion = null; 24 | } 25 | 26 | /** 27 | * Creates a new instance with the specified error message format and operation details. 28 | * 29 | * @param message error message format. 30 | * @param stream the name of the stream. 31 | * @param expectedVersion the expected version of the stream. 32 | */ 33 | public WrongExpectedVersionException(String message, String stream, long expectedVersion) { 34 | super(String.format(message, stream, expectedVersion)); 35 | this.stream = stream; 36 | this.expectedVersion = expectedVersion; 37 | this.currentVersion = null; 38 | } 39 | 40 | /** 41 | * Creates a new instance with the specified error message format and operation details. 42 | * 43 | * @param message error message format. 44 | * @param stream the name of the stream. 45 | * @param expectedVersion the expected version of the stream. 46 | * @param currentVersion the current version of the stream. 47 | */ 48 | public WrongExpectedVersionException(String message, String stream, long expectedVersion, long currentVersion) { 49 | super(String.format(message, stream, expectedVersion, currentVersion)); 50 | this.stream = stream; 51 | this.expectedVersion = expectedVersion; 52 | this.currentVersion = currentVersion; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/util/IntRange.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.util; 2 | 3 | import static com.github.msemys.esjc.util.Preconditions.checkArgument; 4 | 5 | public class IntRange { 6 | public enum Type { 7 | /** 8 | * (a..b) = { x | a < x < b } 9 | */ 10 | OPEN, 11 | 12 | /** 13 | * [a..b] = { x | a <= x <= b } 14 | */ 15 | CLOSED, 16 | 17 | /** 18 | * (a..b] = { x | a < x <= b } 19 | */ 20 | OPEN_CLOSED, 21 | 22 | /** 23 | * [a..b) = { x | a <= x < b } 24 | */ 25 | CLOSED_OPEN 26 | } 27 | 28 | public final int min; 29 | public final int max; 30 | public final Type type; 31 | private String string; 32 | 33 | IntRange(int min, int max, Type type) { 34 | if (type == Type.OPEN) { 35 | checkArgument(min < max, "min should be less than max"); 36 | } else { 37 | checkArgument(min <= max, "min should be less or equal to max"); 38 | } 39 | 40 | this.min = min; 41 | this.max = max; 42 | this.type = type; 43 | } 44 | 45 | public boolean contains(int value) { 46 | switch (type) { 47 | case OPEN: 48 | return min < value && value < max; 49 | case CLOSED: 50 | return min <= value && value <= max; 51 | case OPEN_CLOSED: 52 | return min < value && value <= max; 53 | case CLOSED_OPEN: 54 | return min <= value && value < max; 55 | default: 56 | throw new IllegalStateException("Unexpected type: " + type); 57 | } 58 | } 59 | 60 | @Override 61 | public String toString() { 62 | if (string == null) { 63 | StringBuilder sb = new StringBuilder(); 64 | sb.append(type == Type.OPEN || type == Type.OPEN_CLOSED ? "(" : "["); 65 | sb.append(min == Integer.MIN_VALUE ? "-infinity" : min); 66 | sb.append(".."); 67 | sb.append(max == Integer.MAX_VALUE ? "infinity" : max); 68 | sb.append(type == Type.OPEN || type == Type.CLOSED_OPEN ? ")" : "]"); 69 | string = sb.toString(); 70 | } 71 | return string; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/event/EventQueue.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.event; 2 | 3 | import com.github.msemys.esjc.EventStoreListener; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.util.Queue; 8 | import java.util.Set; 9 | import java.util.concurrent.ConcurrentLinkedQueue; 10 | import java.util.concurrent.CopyOnWriteArraySet; 11 | import java.util.concurrent.Executor; 12 | import java.util.concurrent.atomic.AtomicBoolean; 13 | 14 | import static com.github.msemys.esjc.util.Preconditions.checkNotNull; 15 | 16 | public class EventQueue { 17 | private static final Logger logger = LoggerFactory.getLogger(EventQueue.class); 18 | 19 | private final Executor executor; 20 | private final Queue queue = new ConcurrentLinkedQueue<>(); 21 | private final Set listeners = new CopyOnWriteArraySet<>(); 22 | private final AtomicBoolean processing = new AtomicBoolean(); 23 | 24 | public EventQueue(Executor executor) { 25 | this.executor = executor; 26 | } 27 | 28 | public void register(EventStoreListener listener) { 29 | listeners.add(listener); 30 | } 31 | 32 | public void unregister(EventStoreListener listener) { 33 | listeners.remove(listener); 34 | } 35 | 36 | public void enqueue(Event event) { 37 | checkNotNull(event, "event is null"); 38 | 39 | queue.offer(event); 40 | 41 | if (processing.compareAndSet(false, true)) { 42 | executor.execute(this::process); 43 | } 44 | } 45 | 46 | private void process() { 47 | do { 48 | Event event; 49 | 50 | while ((event = queue.poll()) != null) { 51 | for (EventStoreListener listener : listeners) { 52 | try { 53 | listener.onEvent(event); 54 | } catch (Exception e) { 55 | logger.error("Error occurred while handling '{}' event in {}", 56 | event.getClass().getSimpleName(), 57 | listener.getClass().getName(), 58 | e); 59 | } 60 | } 61 | } 62 | 63 | processing.set(false); 64 | } while (!queue.isEmpty() && processing.compareAndSet(false, true)); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/RecordedEvent.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.github.msemys.esjc.proto.EventStoreClientMessages.EventRecord; 4 | 5 | import java.time.Instant; 6 | import java.util.UUID; 7 | 8 | import static com.github.msemys.esjc.util.EmptyArrays.EMPTY_BYTES; 9 | import static com.github.msemys.esjc.util.UUIDConverter.toUUID; 10 | import static java.time.Instant.ofEpochMilli; 11 | 12 | /** 13 | * Represents a previously written event. 14 | */ 15 | public class RecordedEvent { 16 | 17 | /** 18 | * The event stream that this event belongs to. 19 | */ 20 | public final String eventStreamId; 21 | 22 | /** 23 | * The unique identifier representing this event. 24 | */ 25 | public final UUID eventId; 26 | 27 | /** 28 | * The number of this event in the stream. 29 | */ 30 | public final long eventNumber; 31 | 32 | /** 33 | * The type of event. 34 | */ 35 | public final String eventType; 36 | 37 | /** 38 | * A byte array representing the data of this event. 39 | */ 40 | public final byte[] data; 41 | 42 | /** 43 | * A byte array representing the metadata associated with this event. 44 | */ 45 | public final byte[] metadata; 46 | 47 | /** 48 | * Indicates whether the content is internally marked as JSON. 49 | */ 50 | public final boolean isJson; 51 | 52 | /** 53 | * A datetime representing when this event was created in the system. 54 | */ 55 | public final Instant created; 56 | 57 | /** 58 | * Creates new instance from proto message. 59 | * 60 | * @param eventRecord event record. 61 | */ 62 | public RecordedEvent(EventRecord eventRecord) { 63 | eventStreamId = eventRecord.getEventStreamId(); 64 | 65 | eventId = toUUID(eventRecord.getEventId().toByteArray()); 66 | eventNumber = eventRecord.getEventNumber(); 67 | 68 | eventType = eventRecord.getEventType(); 69 | 70 | data = (eventRecord.hasData()) ? eventRecord.getData().toByteArray() : EMPTY_BYTES; 71 | metadata = (eventRecord.hasMetadata()) ? eventRecord.getMetadata().toByteArray() : EMPTY_BYTES; 72 | isJson = eventRecord.getDataContentType() == 1; 73 | 74 | created = eventRecord.hasCreatedEpoch() ? ofEpochMilli(eventRecord.getCreatedEpoch()) : null; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/node/single/SingleEndpointDiscoverer.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.node.single; 2 | 3 | import com.github.msemys.esjc.node.EndpointDiscoverer; 4 | import com.github.msemys.esjc.node.NodeEndpoints; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.net.InetAddress; 9 | import java.net.InetSocketAddress; 10 | import java.net.UnknownHostException; 11 | import java.util.concurrent.CompletableFuture; 12 | 13 | import static com.github.msemys.esjc.util.Preconditions.checkNotNull; 14 | import static io.netty.util.NetUtil.createByteArrayFromIpAddressString; 15 | import static java.util.concurrent.CompletableFuture.completedFuture; 16 | 17 | public class SingleEndpointDiscoverer implements EndpointDiscoverer { 18 | private static final Logger logger = LoggerFactory.getLogger(SingleEndpointDiscoverer.class); 19 | 20 | private final String hostname; 21 | private final InetAddress ipAddress; 22 | private final int port; 23 | private final boolean ssl; 24 | 25 | public SingleEndpointDiscoverer(SingleNodeSettings settings, boolean ssl) { 26 | checkNotNull(settings, "settings is null"); 27 | 28 | hostname = settings.address.getHostString(); 29 | ipAddress = maybeIpAddress(hostname); 30 | port = settings.address.getPort(); 31 | this.ssl = ssl; 32 | } 33 | 34 | @Override 35 | public CompletableFuture discover(InetSocketAddress failedTcpEndpoint) { 36 | InetSocketAddress address = (ipAddress != null) ? 37 | new InetSocketAddress(ipAddress, port) : 38 | new InetSocketAddress(hostname, port); 39 | 40 | return completedFuture(new NodeEndpoints( 41 | ssl ? null : address, 42 | ssl ? address : null)); 43 | } 44 | 45 | private static InetAddress maybeIpAddress(String address) { 46 | if (address == null) { 47 | return null; 48 | } 49 | 50 | byte[] ipAddressBytes = createByteArrayFromIpAddressString(address); 51 | 52 | if (ipAddressBytes != null) { 53 | try { 54 | return InetAddress.getByAddress(ipAddressBytes); 55 | } catch (UnknownHostException e) { 56 | logger.warn("Unable to resolve IP address by '{}'", address, e); 57 | } 58 | } 59 | 60 | return null; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/operation/InspectionResult.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.operation; 2 | 3 | import java.net.InetSocketAddress; 4 | 5 | import static com.github.msemys.esjc.util.Preconditions.checkArgument; 6 | import static com.github.msemys.esjc.util.Preconditions.checkNotNull; 7 | 8 | public class InspectionResult { 9 | public final InspectionDecision decision; 10 | public final String description; 11 | public final InetSocketAddress address; 12 | public final InetSocketAddress secureAddress; 13 | 14 | private InspectionResult(Builder builder) { 15 | this.decision = builder.decision; 16 | this.description = builder.description; 17 | this.address = builder.address; 18 | this.secureAddress = builder.secureAddress; 19 | } 20 | 21 | public static Builder newBuilder() { 22 | return new Builder(); 23 | } 24 | 25 | public static class Builder { 26 | private InspectionDecision decision; 27 | private String description; 28 | private InetSocketAddress address; 29 | private InetSocketAddress secureAddress; 30 | 31 | private Builder() { 32 | } 33 | 34 | public Builder decision(InspectionDecision decision) { 35 | this.decision = decision; 36 | return this; 37 | } 38 | 39 | public Builder description(String description) { 40 | this.description = description; 41 | return this; 42 | } 43 | 44 | public Builder address(String hostname, int port) { 45 | this.address = new InetSocketAddress(hostname, port); 46 | return this; 47 | } 48 | 49 | public Builder secureAddress(String hostname, int port) { 50 | this.secureAddress = new InetSocketAddress(hostname, port); 51 | return this; 52 | } 53 | 54 | public InspectionResult build() { 55 | checkNotNull(decision, "Decision not specified."); 56 | checkNotNull(description, "Description not specified."); 57 | 58 | if (decision == InspectionDecision.Reconnect) { 59 | checkNotNull(address, "Address not specified."); 60 | } else { 61 | checkArgument(address == null, "Address is specified for decision %s.", decision); 62 | } 63 | 64 | return new InspectionResult(this); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/task/TaskQueue.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.task; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.Queue; 9 | import java.util.concurrent.ConcurrentLinkedQueue; 10 | import java.util.concurrent.Executor; 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | import java.util.function.Consumer; 13 | 14 | import static com.github.msemys.esjc.util.Preconditions.checkNotNull; 15 | import static com.github.msemys.esjc.util.Preconditions.checkState; 16 | 17 | public class TaskQueue { 18 | private static final Logger logger = LoggerFactory.getLogger(TaskQueue.class); 19 | 20 | private final Executor executor; 21 | private final Queue queue = new ConcurrentLinkedQueue<>(); 22 | private final Map, Consumer> handlers = new HashMap<>(); 23 | private final AtomicBoolean processing = new AtomicBoolean(); 24 | 25 | public TaskQueue(Executor executor) { 26 | this.executor = executor; 27 | } 28 | 29 | @SuppressWarnings("unchecked") 30 | public void register(Class type, Consumer handler) { 31 | checkNotNull(type, "type is null"); 32 | checkNotNull(handler, "handler is null"); 33 | handlers.put(type, (Consumer) handler); 34 | } 35 | 36 | public void enqueue(Task task) { 37 | checkNotNull(task, "task is null"); 38 | 39 | queue.offer(task); 40 | 41 | if (processing.compareAndSet(false, true)) { 42 | executor.execute(this::process); 43 | } 44 | } 45 | 46 | private void process() { 47 | do { 48 | Task task; 49 | 50 | while ((task = queue.poll()) != null) { 51 | try { 52 | Consumer handler = handlers.get(task.getClass()); 53 | checkState(handler != null, "No handler registered for task '%s'", task.getClass().getSimpleName()); 54 | handler.accept(task); 55 | } catch (Exception e) { 56 | logger.error("Failed processing task: {}", task.getClass().getSimpleName(), e); 57 | task.fail(e); 58 | } 59 | } 60 | 61 | processing.set(false); 62 | } while (!queue.isEmpty() && processing.compareAndSet(false, true)); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/ITSslCommonNameConnection.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.github.msemys.esjc.event.ClientConnected; 4 | import com.github.msemys.esjc.event.ErrorOccurred; 5 | import org.junit.Test; 6 | 7 | import java.security.cert.CertificateException; 8 | import java.util.concurrent.CountDownLatch; 9 | import java.util.concurrent.atomic.AtomicReference; 10 | 11 | import static org.hamcrest.CoreMatchers.containsString; 12 | import static org.hamcrest.CoreMatchers.instanceOf; 13 | import static org.junit.Assert.fail; 14 | import static org.junit.internal.matchers.ThrowableMessageMatcher.hasMessage; 15 | 16 | public class ITSslCommonNameConnection extends AbstractSslConnectionTest { 17 | 18 | @Test 19 | public void connectsWithMatchingCommonName() throws InterruptedException { 20 | EventStore eventstore = createEventStore("localhost"); 21 | 22 | CountDownLatch signal = new CountDownLatch(1); 23 | eventstore.addListener(onEvent(ClientConnected.class, e -> signal.countDown())); 24 | 25 | eventstore.connect(); 26 | 27 | assertSignal(signal); 28 | 29 | eventstore.shutdown(); 30 | } 31 | 32 | @Test 33 | public void failsWithNonMatchingCommonName() throws InterruptedException { 34 | EventStore eventstore = createEventStore("somethingelse"); 35 | 36 | CountDownLatch signal = new CountDownLatch(1); 37 | AtomicReference error = new AtomicReference<>(); 38 | 39 | eventstore.addListener(onEvent(ErrorOccurred.class, e -> { 40 | error.set(e.throwable); 41 | signal.countDown(); 42 | })); 43 | 44 | eventstore.connect(); 45 | 46 | assertSignal(signal); 47 | assertThrowable(error.get(), 48 | instanceOf(CertificateException.class), 49 | hasMessage(containsString("server certificate common name (CN) mismatch"))); 50 | 51 | eventstore.shutdown(); 52 | } 53 | 54 | @Test 55 | public void failsWithEmptyCommonName() { 56 | expectedException.expect(IllegalArgumentException.class); 57 | expectedException.expectMessage("certificateCommonName is null or empty"); 58 | 59 | createEventStore(""); 60 | 61 | fail("Exception expected!"); 62 | } 63 | 64 | private static EventStore createEventStore(final String commonName) { 65 | return newEventStoreBuilder().useSslConnection(commonName).build(); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/ITReadAllEventsForwardWithSoftDeletedStream.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.github.msemys.esjc.system.SystemEventTypes; 4 | import org.junit.Test; 5 | 6 | import java.util.List; 7 | 8 | import static com.github.msemys.esjc.matcher.RecordedEventListMatcher.containsInOrder; 9 | import static com.github.msemys.esjc.system.SystemStreams.metastreamOf; 10 | import static java.util.stream.Collectors.toList; 11 | import static org.junit.Assert.*; 12 | 13 | public class ITReadAllEventsForwardWithSoftDeletedStream extends AbstractEventStoreTest { 14 | 15 | private static final Long DELETED_STREAM_EVENT_NUMBER = Long.MAX_VALUE; 16 | 17 | public ITReadAllEventsForwardWithSoftDeletedStream(EventStore eventstore) { 18 | super(eventstore); 19 | } 20 | 21 | @Test 22 | public void ensuresDeletedStream() { 23 | final String stream = generateStreamName(); 24 | 25 | eventstore.appendToStream(stream, ExpectedVersion.NO_STREAM, newTestEvents(20)).join(); 26 | eventstore.deleteStream(stream, ExpectedVersion.ANY).join(); 27 | 28 | StreamEventsSlice slice = eventstore.readStreamEventsForward(stream, 0, 100, false).join(); 29 | 30 | assertEquals(SliceReadStatus.StreamNotFound, slice.status); 31 | assertTrue(slice.events.isEmpty()); 32 | } 33 | 34 | @Test 35 | public void returnsAllEventsIncludingTombstone() { 36 | final String stream = generateStreamName(); 37 | 38 | List events = newTestEvents(20); 39 | Position position = eventstore.appendToStream(stream, ExpectedVersion.NO_STREAM, events.get(0)).join().logPosition; 40 | eventstore.appendToStream(stream, 0, events.stream().skip(1).collect(toList())).join(); 41 | eventstore.deleteStream(stream, ExpectedVersion.ANY).join(); 42 | 43 | AllEventsSlice slice = eventstore.readAllEventsForward(position, events.size() + 10, false).join(); 44 | 45 | assertThat(slice.events.stream().limit(events.size()).map(e -> e.event).collect(toList()), containsInOrder(events)); 46 | 47 | RecordedEvent lastEvent = slice.events.get(slice.events.size() - 1).event; 48 | assertEquals(metastreamOf(stream), lastEvent.eventStreamId); 49 | assertEquals(SystemEventTypes.STREAM_METADATA, lastEvent.eventType); 50 | 51 | StreamMetadata metadata = StreamMetadata.fromJson(lastEvent.data); 52 | assertEquals(DELETED_STREAM_EVENT_NUMBER, metadata.truncateBefore); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/ITJsonFlagOnEventData.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import org.junit.Test; 4 | 5 | import static java.util.Arrays.asList; 6 | import static org.hamcrest.core.Is.is; 7 | import static org.junit.Assert.*; 8 | 9 | public class ITJsonFlagOnEventData extends AbstractEventStoreTest { 10 | 11 | public ITJsonFlagOnEventData(EventStore eventstore) { 12 | super(eventstore); 13 | } 14 | 15 | @Test 16 | public void setsJsonFlag() { 17 | final String stream = generateStreamName(); 18 | 19 | eventstore.appendToStream(stream, ExpectedVersion.NO_STREAM, asList( 20 | EventData.newBuilder() 21 | .type("some-type") 22 | .jsonData("{\"some\":\"json\"}") 23 | .jsonMetadata((String) null) 24 | .build(), 25 | EventData.newBuilder() 26 | .type("some-type") 27 | .jsonData((String) null) 28 | .jsonMetadata("{\"some\":\"json\"}") 29 | .build(), 30 | EventData.newBuilder() 31 | .type("some-type") 32 | .jsonData("{\"some\":\"json\"}") 33 | .jsonMetadata("{\"some\":\"json\"}") 34 | .build() 35 | )).join(); 36 | 37 | try (Transaction transaction = eventstore.startTransaction(stream, ExpectedVersion.ANY).join()) { 38 | transaction.write(asList( 39 | EventData.newBuilder() 40 | .type("some-type") 41 | .jsonData("{\"some\":\"json\"}") 42 | .jsonMetadata((String) null) 43 | .build(), 44 | EventData.newBuilder() 45 | .type("some-type") 46 | .jsonData((String) null) 47 | .jsonMetadata("{\"some\":\"json\"}") 48 | .build(), 49 | EventData.newBuilder() 50 | .type("some-type") 51 | .jsonData("{\"some\":\"json\"}") 52 | .jsonMetadata("{\"some\":\"json\"}") 53 | .build() 54 | )).join(); 55 | transaction.commit().join(); 56 | } catch (Exception e) { 57 | fail(e.getMessage()); 58 | } 59 | 60 | StreamEventsSlice result = eventstore.readStreamEventsForward(stream, 0, 100, false).join(); 61 | 62 | assertThat(result.events.size(), is(6)); 63 | result.events.forEach(e -> assertTrue("Event #" + e.originalEvent().eventNumber, e.originalEvent().isJson)); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/StreamEventsSlice.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.github.msemys.esjc.proto.EventStoreClientMessages.ResolvedIndexedEvent; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import static com.github.msemys.esjc.util.Preconditions.checkArgument; 9 | import static com.github.msemys.esjc.util.Strings.isNullOrEmpty; 10 | import static java.util.Collections.emptyList; 11 | import static java.util.stream.Collectors.toCollection; 12 | 13 | /** 14 | * Result type of a single read operation to the Event Store, that retrieves event list from stream. 15 | */ 16 | public class StreamEventsSlice { 17 | 18 | /** 19 | * Status of read attempt. 20 | */ 21 | public final SliceReadStatus status; 22 | 23 | /** 24 | * The name of the stream read. 25 | */ 26 | public final String stream; 27 | 28 | /** 29 | * The starting point (represented as a sequence number) of the read operation. 30 | */ 31 | public final long fromEventNumber; 32 | 33 | /** 34 | * The direction of read request. 35 | */ 36 | public final ReadDirection readDirection; 37 | 38 | /** 39 | * The events read. 40 | */ 41 | public final List events; 42 | 43 | /** 44 | * The next event number that can be read. 45 | */ 46 | public final long nextEventNumber; 47 | 48 | /** 49 | * The last event number in the stream. 50 | */ 51 | public final long lastEventNumber; 52 | 53 | /** 54 | * Indicating whether or not this is the end of the stream. 55 | */ 56 | public final boolean isEndOfStream; 57 | 58 | public StreamEventsSlice(SliceReadStatus status, 59 | String stream, 60 | long fromEventNumber, 61 | ReadDirection readDirection, 62 | List events, 63 | long nextEventNumber, 64 | long lastEventNumber, 65 | boolean isEndOfStream) { 66 | checkArgument(!isNullOrEmpty(stream), "stream is null or empty"); 67 | this.status = status; 68 | this.stream = stream; 69 | this.fromEventNumber = fromEventNumber; 70 | this.readDirection = readDirection; 71 | this.events = (events == null) ? emptyList() : events.stream() 72 | .map(ResolvedEvent::new) 73 | .collect(toCollection(() -> new ArrayList<>(events.size()))); 74 | this.nextEventNumber = nextEventNumber; 75 | this.lastEventNumber = lastEventNumber; 76 | this.isEndOfStream = isEndOfStream; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/projection/UpdateOptions.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.projection; 2 | 3 | /** 4 | * Update operation options. 5 | */ 6 | public class UpdateOptions { 7 | 8 | /** 9 | * Updates projection query without changing the current emit option. 10 | */ 11 | public static final UpdateOptions QUERY_ONLY = newBuilder().ignoreEmit().build(); 12 | 13 | /** 14 | * Updates projection query and enables emit option, that allows projection to write to streams. 15 | */ 16 | public static final UpdateOptions EMIT_ENABLED = newBuilder().emit(true).build(); 17 | 18 | /** 19 | * Updates projection query and disables emit option, that denies projection to write to streams. 20 | */ 21 | public static final UpdateOptions EMIT_DISABLED = newBuilder().emit(false).build(); 22 | 23 | 24 | /** 25 | * Whether or not the projection is allowed to write to streams. 26 | *

Note: option ignored when {@code null} 27 | */ 28 | public final Boolean emit; 29 | 30 | private UpdateOptions(Builder builder) { 31 | emit = builder.emit; 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | final StringBuilder sb = new StringBuilder("UpdateOptions{"); 37 | sb.append("emit=").append(emit); 38 | sb.append('}'); 39 | return sb.toString(); 40 | } 41 | 42 | /** 43 | * Creates a new update operation options builder. 44 | * 45 | * @return update operation options builder 46 | */ 47 | public static Builder newBuilder() { 48 | return new Builder(); 49 | } 50 | 51 | /** 52 | * Update operation options builder. 53 | */ 54 | public static class Builder { 55 | private Boolean emit; 56 | 57 | /** 58 | * Specifies whether or not the projection is allowed to write to streams (by default, it is ignored). 59 | * 60 | * @param enabled {@code true} to allow to write to streams. 61 | * @return the builder reference 62 | * @see #ignoreEmit() 63 | */ 64 | public Builder emit(boolean enabled) { 65 | this.emit = enabled; 66 | return this; 67 | } 68 | 69 | /** 70 | * Ignores emit option modification. 71 | * 72 | * @return the builder reference 73 | */ 74 | public Builder ignoreEmit() { 75 | emit = null; 76 | return this; 77 | } 78 | 79 | /** 80 | * Builds an update operation options. 81 | * 82 | * @return update operation options 83 | */ 84 | public UpdateOptions build() { 85 | return new UpdateOptions(this); 86 | } 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/ResolvedEvent.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.github.msemys.esjc.proto.EventStoreClientMessages; 4 | 5 | /** 6 | * Structure represents a single event or an resolved link event. 7 | */ 8 | public class ResolvedEvent { 9 | 10 | /** 11 | * The event, or the resolved link event if this is a link event. 12 | */ 13 | public final RecordedEvent event; 14 | 15 | /** 16 | * The link event if this is a link event. 17 | */ 18 | public final RecordedEvent link; 19 | 20 | /** 21 | * The logical position of the original event. 22 | * 23 | * @see #originalEvent() 24 | */ 25 | public final Position originalPosition; 26 | 27 | /** 28 | * Creates new instance from proto message. 29 | * 30 | * @param event resolved event. 31 | */ 32 | public ResolvedEvent(EventStoreClientMessages.ResolvedEvent event) { 33 | this.event = (event.hasEvent()) ? new RecordedEvent(event.getEvent()) : null; 34 | this.link = (event.hasLink()) ? new RecordedEvent(event.getLink()) : null; 35 | this.originalPosition = new Position(event.getCommitPosition(), event.getPreparePosition()); 36 | } 37 | 38 | /** 39 | * Creates new instance from proto message. 40 | * 41 | * @param event resolved indexed event. 42 | */ 43 | public ResolvedEvent(EventStoreClientMessages.ResolvedIndexedEvent event) { 44 | this.event = (event.hasEvent()) ? new RecordedEvent(event.getEvent()) : null; 45 | this.link = (event.hasLink()) ? new RecordedEvent(event.getLink()) : null; 46 | this.originalPosition = null; 47 | } 48 | 49 | /** 50 | * Indicates whether this event is a resolved link event. 51 | * 52 | * @return {@code true} if it is a resolved link event, otherwise {@code false} 53 | */ 54 | public boolean isResolved() { 55 | return (link != null) && (event != null); 56 | } 57 | 58 | /** 59 | * Gets event that was read or which triggered the subscription. 60 | * 61 | * @return the {@link #link} when it represents a link event, otherwise {@link #event} 62 | */ 63 | public RecordedEvent originalEvent() { 64 | return (link != null) ? link : event; 65 | } 66 | 67 | /** 68 | * The stream name of the original event. 69 | * 70 | * @return stream name 71 | * @see #originalEvent() 72 | */ 73 | public String originalStreamId() { 74 | return originalEvent().eventStreamId; 75 | } 76 | 77 | /** 78 | * The event number in the stream of original event. 79 | * 80 | * @return event number 81 | * @see #originalEvent() 82 | */ 83 | public long originalEventNumber() { 84 | return originalEvent().eventNumber; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/matcher/RecordedEventListMatcher.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.matcher; 2 | 3 | import com.github.msemys.esjc.EventData; 4 | import com.github.msemys.esjc.RecordedEvent; 5 | import org.hamcrest.Description; 6 | import org.hamcrest.Factory; 7 | import org.hamcrest.Matcher; 8 | import org.hamcrest.TypeSafeMatcher; 9 | 10 | import java.util.List; 11 | 12 | public class RecordedEventListMatcher extends TypeSafeMatcher> { 13 | private final List expected; 14 | private Matcher elementMatcher; 15 | private int elementIndex; 16 | 17 | public RecordedEventListMatcher(List expected) { 18 | this.expected = expected; 19 | } 20 | 21 | @Override 22 | protected boolean matchesSafely(List actual) { 23 | if (expected.size() == actual.size()) { 24 | for (int i = 0; i < expected.size(); i++) { 25 | EventData expectedItem = expected.get(i); 26 | RecordedEvent actualItem = actual.get(i); 27 | 28 | elementMatcher = RecordedEventMatcher.equalTo(expectedItem); 29 | elementIndex = i; 30 | 31 | if (!elementMatcher.matches(actualItem)) { 32 | return false; 33 | } 34 | } 35 | 36 | return true; 37 | } else { 38 | return false; 39 | } 40 | } 41 | 42 | @Override 43 | public void describeTo(Description description) { 44 | description.appendText("a collection with size ").appendValue(expected.size()); 45 | 46 | if (elementMatcher != null) { 47 | description 48 | .appendText(" contains ") 49 | .appendDescriptionOf(elementMatcher) 50 | .appendText(" at index ") 51 | .appendValue(elementIndex); 52 | } 53 | } 54 | 55 | @Override 56 | protected void describeMismatchSafely(List actual, Description mismatchDescription) { 57 | mismatchDescription.appendText("collection with size ").appendValue(actual.size()); 58 | 59 | if (elementMatcher != null && elementIndex < actual.size()) { 60 | RecordedEvent actualItem = actual.get(elementIndex); 61 | 62 | mismatchDescription 63 | .appendText(" has event ") 64 | .appendValue(actualItem.eventType) 65 | .appendText(" ") 66 | .appendValue(actualItem.eventId) 67 | .appendText(" at index ") 68 | .appendValue(elementIndex); 69 | } 70 | } 71 | 72 | @Factory 73 | public static Matcher> containsInOrder(List items) { 74 | return new RecordedEventListMatcher(items); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/tcp/TcpCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.tcp; 2 | 3 | public enum TcpCommand { 4 | 5 | HeartbeatRequestCommand(0x01), 6 | HeartbeatResponseCommand(0x02), 7 | 8 | Ping(0x03), 9 | Pong(0x04), 10 | 11 | PrepareAck(0x05), 12 | CommitAck(0x06), 13 | 14 | SlaveAssignment(0x07), 15 | CloneAssignment(0x08), 16 | 17 | SubscribeReplica(0x10), 18 | ReplicaLogPositionAck(0x11), 19 | CreateChunk(0x12), 20 | RawChunkBulk(0x13), 21 | DataChunkBulk(0x14), 22 | ReplicaSubscriptionRetry(0x15), 23 | ReplicaSubscribed(0x16), 24 | 25 | // CLIENT COMMANDS 26 | // CreateStream = 0x80, 27 | // CreateStreamCompleted = 0x81, 28 | 29 | WriteEvents(0x82), 30 | WriteEventsCompleted(0x83), 31 | 32 | TransactionStart(0x84), 33 | TransactionStartCompleted(0x85), 34 | TransactionWrite(0x86), 35 | TransactionWriteCompleted(0x87), 36 | TransactionCommit(0x88), 37 | TransactionCommitCompleted(0x89), 38 | 39 | DeleteStream(0x8A), 40 | DeleteStreamCompleted(0x8B), 41 | 42 | ReadEvent(0xB0), 43 | ReadEventCompleted(0xB1), 44 | ReadStreamEventsForward(0xB2), 45 | ReadStreamEventsForwardCompleted(0xB3), 46 | ReadStreamEventsBackward(0xB4), 47 | ReadStreamEventsBackwardCompleted(0xB5), 48 | ReadAllEventsForward(0xB6), 49 | ReadAllEventsForwardCompleted(0xB7), 50 | ReadAllEventsBackward(0xB8), 51 | ReadAllEventsBackwardCompleted(0xB9), 52 | 53 | SubscribeToStream(0xC0), 54 | SubscriptionConfirmation(0xC1), 55 | StreamEventAppeared(0xC2), 56 | UnsubscribeFromStream(0xC3), 57 | SubscriptionDropped(0xC4), 58 | ConnectToPersistentSubscription(0xC5), 59 | PersistentSubscriptionConfirmation(0xC6), 60 | PersistentSubscriptionStreamEventAppeared(0xC7), 61 | CreatePersistentSubscription(0xC8), 62 | CreatePersistentSubscriptionCompleted(0xC9), 63 | DeletePersistentSubscription(0xCA), 64 | DeletePersistentSubscriptionCompleted(0xCB), 65 | PersistentSubscriptionAckEvents(0xCC), 66 | PersistentSubscriptionNakEvents(0xCD), 67 | UpdatePersistentSubscription(0xCE), 68 | UpdatePersistentSubscriptionCompleted(0xCF), 69 | 70 | ScavengeDatabase(0xD0), 71 | ScavengeDatabaseCompleted(0xD1), 72 | 73 | BadRequest(0xF0), 74 | NotHandled(0xF1), 75 | Authenticate(0xF2), 76 | Authenticated(0xF3), 77 | NotAuthenticated(0xF4), 78 | IdentifyClient(0xF5), 79 | ClientIdentified(0xF6); 80 | 81 | public final byte value; 82 | 83 | TcpCommand(int value) { 84 | this.value = (byte) value; 85 | } 86 | 87 | public static TcpCommand of(byte value) { 88 | for (TcpCommand c : TcpCommand.values()) { 89 | if (c.value == value) { 90 | return c; 91 | } 92 | } 93 | throw new IllegalArgumentException(String.format("Unsupported TCP command %s", Integer.toHexString(value))); 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/ITSubscribeToStream.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.concurrent.CountDownLatch; 6 | 7 | import static java.util.concurrent.TimeUnit.SECONDS; 8 | import static org.junit.Assert.assertTrue; 9 | 10 | public class ITSubscribeToStream extends AbstractEventStoreTest { 11 | 12 | public ITSubscribeToStream(EventStore eventstore) { 13 | super(eventstore); 14 | } 15 | 16 | @Test 17 | public void subscribesToNonExistingStreamAndThenCatchesNewEvent() throws InterruptedException { 18 | final String stream = generateStreamName(); 19 | 20 | CountDownLatch eventSignal = new CountDownLatch(1); 21 | 22 | eventstore.subscribeToStream(stream, false, (s, e) -> eventSignal.countDown()).join(); 23 | eventstore.appendToStream(stream, ExpectedVersion.NO_STREAM, newTestEvent()).join(); 24 | 25 | assertTrue("onEvent timeout", eventSignal.await(10, SECONDS)); 26 | } 27 | 28 | @Test 29 | public void allowsMultipleSubscriptionsToSameStream() throws InterruptedException { 30 | final String stream = generateStreamName(); 31 | 32 | CountDownLatch eventSignal = new CountDownLatch(2); 33 | 34 | eventstore.subscribeToStream(stream, false, (s, e) -> eventSignal.countDown()).join(); 35 | eventstore.subscribeToStream(stream, false, (s, e) -> eventSignal.countDown()).join(); 36 | eventstore.appendToStream(stream, ExpectedVersion.NO_STREAM, newTestEvent()).join(); 37 | 38 | assertTrue("onEvent timeout", eventSignal.await(10, SECONDS)); 39 | } 40 | 41 | @Test 42 | public void triggersOnCloseCallbackAfterUnsubscribeMethodCall() throws InterruptedException { 43 | final String stream = generateStreamName(); 44 | 45 | CountDownLatch closeSignal = new CountDownLatch(1); 46 | 47 | Subscription subscription = eventstore.subscribeToStream(stream, false, new VolatileSubscriptionListener() { 48 | @Override 49 | public void onEvent(Subscription subscription, ResolvedEvent event) { 50 | 51 | } 52 | 53 | @Override 54 | public void onClose(Subscription subscription, SubscriptionDropReason reason, Exception exception) { 55 | closeSignal.countDown(); 56 | } 57 | }).join(); 58 | 59 | subscription.unsubscribe(); 60 | 61 | assertTrue("onClose timeout", closeSignal.await(10, SECONDS)); 62 | } 63 | 64 | @Test 65 | public void catchesDeletedEventsAsWell() throws InterruptedException { 66 | final String stream = generateStreamName(); 67 | 68 | CountDownLatch eventSignal = new CountDownLatch(1); 69 | 70 | eventstore.subscribeToStream(stream, false, (s, e) -> eventSignal.countDown()).join(); 71 | eventstore.deleteStream(stream, ExpectedVersion.NO_STREAM, true).join(); 72 | 73 | assertTrue("onEvent timeout", eventSignal.await(10, SECONDS)); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/tcp/handler/HeartbeatHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.tcp.handler; 2 | 3 | import com.github.msemys.esjc.tcp.TcpCommand; 4 | import com.github.msemys.esjc.tcp.TcpPackage; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.channel.SimpleChannelInboundHandler; 7 | import io.netty.handler.timeout.IdleStateEvent; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.time.Duration; 12 | import java.util.UUID; 13 | import java.util.concurrent.ScheduledFuture; 14 | 15 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 16 | 17 | public class HeartbeatHandler extends SimpleChannelInboundHandler { 18 | private static final Logger logger = LoggerFactory.getLogger(HeartbeatHandler.class); 19 | 20 | private final long timeoutMillis; 21 | private ScheduledFuture timeoutTask; 22 | private final Object timeoutTaskLock = new Object(); 23 | 24 | public HeartbeatHandler(Duration timeout) { 25 | timeoutMillis = timeout.toMillis(); 26 | } 27 | 28 | @Override 29 | protected void channelRead0(ChannelHandlerContext ctx, TcpPackage msg) throws Exception { 30 | switch (msg.command) { 31 | case HeartbeatRequestCommand: 32 | ctx.writeAndFlush(TcpPackage.newBuilder() 33 | .command(TcpCommand.HeartbeatResponseCommand) 34 | .correlationId(msg.correlationId) 35 | .build()); 36 | break; 37 | case HeartbeatResponseCommand: 38 | cancelTimeoutTask(); 39 | break; 40 | default: 41 | ctx.fireChannelRead(msg); 42 | } 43 | } 44 | 45 | @Override 46 | public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { 47 | cancelTimeoutTask(); 48 | } 49 | 50 | @Override 51 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { 52 | if (evt instanceof IdleStateEvent) { 53 | synchronized (timeoutTaskLock) { 54 | if (timeoutTask == null) { 55 | ctx.writeAndFlush(TcpPackage.newBuilder() 56 | .command(TcpCommand.HeartbeatRequestCommand) 57 | .correlationId(UUID.randomUUID()) 58 | .build()); 59 | timeoutTask = ctx.executor().schedule(() -> { 60 | logger.info("Closing TCP connection [{}, L{}] due to HEARTBEAT TIMEOUT.", ctx.channel().remoteAddress(), ctx.channel().localAddress()); 61 | ctx.close(); 62 | }, timeoutMillis, MILLISECONDS); 63 | } 64 | } 65 | } 66 | } 67 | 68 | private void cancelTimeoutTask() { 69 | synchronized (timeoutTaskLock) { 70 | if (timeoutTask != null) { 71 | timeoutTask.cancel(true); 72 | timeoutTask = null; 73 | } 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/AbstractEventsSpliterator.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import java.util.List; 4 | import java.util.Spliterator; 5 | import java.util.concurrent.CompletableFuture; 6 | import java.util.function.Consumer; 7 | import java.util.function.Function; 8 | 9 | import static com.github.msemys.esjc.util.Preconditions.checkNotNull; 10 | import static java.util.Spliterators.emptySpliterator; 11 | 12 | abstract class AbstractEventsSpliterator implements Spliterator { 13 | private static final int CHARACTERISTICS = SIZED | SUBSIZED | IMMUTABLE | ORDERED | NONNULL; 14 | 15 | private final Function> reader; 16 | private T cursor; 17 | private Spliterator spliterator; 18 | private boolean endOfStream; 19 | private long estimate = Long.MAX_VALUE; 20 | 21 | AbstractEventsSpliterator(T cursor, Function> reader) { 22 | this.cursor = cursor; 23 | this.reader = reader; 24 | } 25 | 26 | @Override 27 | public boolean tryAdvance(Consumer action) { 28 | checkNotNull(action, "action is null"); 29 | 30 | if (spliterator == null) { 31 | spliterator = nextBatch().spliterator(); 32 | } 33 | 34 | if (spliterator.tryAdvance(action)) { 35 | return true; 36 | } else if (!endOfStream) { 37 | spliterator = nextBatch().spliterator(); 38 | return spliterator.tryAdvance(action); 39 | } else { 40 | spliterator = emptySpliterator(); 41 | return false; 42 | } 43 | } 44 | 45 | @Override 46 | public Spliterator trySplit() { 47 | if (!endOfStream) { 48 | if (spliterator == null) { 49 | return nextBatch().spliterator(); 50 | } else { 51 | Spliterator currentSpliterator = spliterator; 52 | spliterator = null; 53 | return currentSpliterator; 54 | } 55 | } else { 56 | if (spliterator == null) { 57 | spliterator = emptySpliterator(); 58 | } 59 | return null; 60 | } 61 | } 62 | 63 | @Override 64 | public long estimateSize() { 65 | return estimate; 66 | } 67 | 68 | @Override 69 | public int characteristics() { 70 | return CHARACTERISTICS; 71 | } 72 | 73 | private List nextBatch() { 74 | R slice = reader.apply(cursor).join(); 75 | 76 | onBatchReceived(slice); 77 | 78 | List events = getEvents(slice); 79 | estimate = events.size(); 80 | 81 | return events; 82 | } 83 | 84 | protected void onBatchReceived(R slice) { 85 | cursor = getNextCursor(slice); 86 | endOfStream = isEndOfStream(slice); 87 | } 88 | 89 | protected abstract T getNextCursor(R slice); 90 | 91 | protected abstract List getEvents(R slice); 92 | 93 | protected abstract boolean isEndOfStream(R slice); 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/node/cluster/MemberInfoDto.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.node.cluster; 2 | 3 | import java.time.Instant; 4 | import java.time.ZoneId; 5 | import java.time.format.DateTimeFormatter; 6 | import java.util.UUID; 7 | 8 | public class MemberInfoDto { 9 | private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter 10 | .ofPattern("yyyy-MM-dd HH:mm:ss.SSS") 11 | .withZone(ZoneId.systemDefault()); 12 | 13 | public UUID instanceId; 14 | 15 | public Instant timeStamp; 16 | public VNodeState state; 17 | public boolean isAlive; 18 | 19 | public String internalTcpIp; 20 | public int internalTcpPort; 21 | public int internalSecureTcpPort; 22 | 23 | public String externalTcpIp; 24 | public int externalTcpPort; 25 | public int externalSecureTcpPort; 26 | 27 | public String internalHttpIp; 28 | public int internalHttpPort; 29 | 30 | public String externalHttpIp; 31 | public int externalHttpPort; 32 | 33 | public long lastCommitPosition; 34 | public long writerCheckpoint; 35 | public long chaserCheckpoint; 36 | 37 | public long epochPosition; 38 | public long epochNumber; 39 | public UUID epochId; 40 | 41 | public int nodePriority; 42 | 43 | @Override 44 | public String toString() { 45 | final StringBuilder sb = new StringBuilder(); 46 | 47 | if (state == VNodeState.Manager) { 48 | sb.append("MAN ") 49 | .append(instanceId.toString()) 50 | .append(" <").append(isAlive ? "LIVE" : "DEAD").append("> ") 51 | .append("[").append(state).append(", ") 52 | .append(internalHttpIp).append(":").append(internalHttpPort).append(", ") 53 | .append(externalHttpIp).append(":").append(externalHttpPort).append("] | ") 54 | .append(TIMESTAMP_FORMATTER.format(timeStamp)); 55 | } else { 56 | sb.append("VND ") 57 | .append(instanceId.toString()) 58 | .append(" <").append(isAlive ? "LIVE" : "DEAD").append("> ") 59 | .append("[").append(state).append(", ") 60 | .append(internalTcpIp).append(":").append(internalTcpPort).append(", ") 61 | .append(internalSecureTcpPort > 0 ? internalTcpIp + ":" + internalSecureTcpPort : "n/a").append(", ") 62 | .append(externalTcpIp).append(":").append(externalTcpPort).append(", ") 63 | .append(externalSecureTcpPort > 0 ? externalTcpIp + ":" + externalSecureTcpPort : "n/a").append(", ") 64 | .append(internalHttpIp).append(":").append(internalHttpPort).append(", ") 65 | .append(externalHttpIp).append(":").append(externalHttpPort).append("] ") 66 | .append(lastCommitPosition).append("/").append(writerCheckpoint).append("/").append(chaserCheckpoint) 67 | .append("E").append(epochNumber).append("@").append(epochPosition).append(":").append(epochId.toString()) 68 | .append(" | ").append(TIMESTAMP_FORMATTER.format(timeStamp)); 69 | } 70 | 71 | return sb.toString(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/ITDeleteStream.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.github.msemys.esjc.operation.StreamDeletedException; 4 | import com.github.msemys.esjc.operation.WrongExpectedVersionException; 5 | import org.junit.Test; 6 | 7 | import static org.hamcrest.CoreMatchers.instanceOf; 8 | import static org.junit.Assert.*; 9 | 10 | public class ITDeleteStream extends AbstractEventStoreTest { 11 | 12 | public ITDeleteStream(EventStore eventstore) { 13 | super(eventstore); 14 | } 15 | 16 | @Test 17 | public void succeedsToDeleteNonExistentStreamWithNoStreamExpectedVersion() { 18 | final String stream = generateStreamName(); 19 | eventstore.deleteStream(stream, ExpectedVersion.NO_STREAM, true).join(); 20 | } 21 | 22 | @Test 23 | public void succeedsToDeleteNonExistentStreamWithAnyExpectedVersion() { 24 | final String stream = generateStreamName(); 25 | eventstore.deleteStream(stream, ExpectedVersion.ANY, true).join(); 26 | } 27 | 28 | @Test 29 | public void succeedsToDeleteExistingStream() { 30 | final String stream = generateStreamName(); 31 | eventstore.appendToStream(stream, ExpectedVersion.NO_STREAM, newTestEvent()).join(); 32 | eventstore.deleteStream(stream, 0, true).join(); 33 | } 34 | 35 | @Test 36 | public void failsToDeleteNonExistentStreamWithInvalidExpectedVersion() { 37 | final String stream = generateStreamName(); 38 | try { 39 | eventstore.deleteStream(stream, 1, true).join(); 40 | fail("delete should fail with 'WrongExpectedVersionException'"); 41 | } catch (Exception e) { 42 | assertThat(e.getCause(), instanceOf(WrongExpectedVersionException.class)); 43 | } 44 | } 45 | 46 | @Test 47 | public void failsToDeleteAlreadyDeletedStream() { 48 | final String stream = generateStreamName(); 49 | 50 | eventstore.deleteStream(stream, ExpectedVersion.NO_STREAM, true).join(); 51 | 52 | try { 53 | eventstore.deleteStream(stream, ExpectedVersion.ANY, true).join(); 54 | fail("delete should fail with 'StreamDeletedException'"); 55 | } catch (Exception e) { 56 | assertThat(e.getCause(), instanceOf(StreamDeletedException.class)); 57 | } 58 | } 59 | 60 | @Test 61 | public void whenDeleteFailsWrongExpectedVersionExceptionContainsOperationDetails() { 62 | final String stream = generateStreamName(); 63 | 64 | eventstore.appendToStream(stream, ExpectedVersion.NO_STREAM, newTestEvents(3)).join(); 65 | 66 | try { 67 | eventstore.deleteStream(stream, 10, true).join(); 68 | fail("delete should fail with 'WrongExpectedVersionException'"); 69 | } catch (Exception e) { 70 | assertThat(e.getCause(), instanceOf(WrongExpectedVersionException.class)); 71 | 72 | WrongExpectedVersionException cause = (WrongExpectedVersionException) e.getCause(); 73 | assertEquals(stream, cause.stream); 74 | assertEquals(Long.valueOf(10), cause.expectedVersion); 75 | assertNull(cause.currentVersion); 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/com/github/msemys/esjc/ITSslCertificateConnection.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.github.msemys.esjc.event.ClientConnected; 4 | import com.github.msemys.esjc.event.ErrorOccurred; 5 | import org.junit.Test; 6 | 7 | import javax.net.ssl.SSLHandshakeException; 8 | import java.io.File; 9 | import java.util.concurrent.CountDownLatch; 10 | import java.util.concurrent.atomic.AtomicReference; 11 | 12 | import static org.hamcrest.CoreMatchers.instanceOf; 13 | import static org.junit.Assert.fail; 14 | 15 | public class ITSslCertificateConnection extends AbstractSslConnectionTest { 16 | 17 | @Test 18 | public void connectsWithMatchingCertificateFile() throws InterruptedException { 19 | testSuccessfulConnection(new File("ssl/domain.crt")); 20 | } 21 | 22 | @Test 23 | public void connectsWithMatchingCACertificateFile() throws InterruptedException { 24 | testSuccessfulConnection(new File("ssl/rootCA.crt")); 25 | } 26 | 27 | @Test 28 | public void failsWithNonMatchingCertificateFile() throws Throwable { 29 | final File certificateFile = new File("ssl/invalid.crt"); 30 | 31 | EventStore eventstore = createEventStore(certificateFile); 32 | 33 | CountDownLatch signal = new CountDownLatch(1); 34 | AtomicReference error = new AtomicReference<>(); 35 | 36 | eventstore.addListener(onEvent(ErrorOccurred.class, e -> { 37 | error.set(e.throwable); 38 | signal.countDown(); 39 | })); 40 | 41 | eventstore.connect(); 42 | 43 | assertSignal(signal); 44 | assertThrowable(error.get(), instanceOf(SSLHandshakeException.class)); 45 | 46 | eventstore.shutdown(); 47 | } 48 | 49 | @Test 50 | public void failsWithNonExistingCertificateFile() { 51 | final File certificateFile = new File("doesnotexist.csr"); 52 | expectedException.expect(IllegalArgumentException.class); 53 | expectedException.expectMessage("certificateFile '" + certificateFile + "' does not exist"); 54 | 55 | createEventStore(certificateFile); 56 | 57 | fail("Exception expected!"); 58 | } 59 | 60 | @Test 61 | public void failsWithEmptyCertificateFileName() { 62 | final File certificateFile = new File(""); 63 | expectedException.expect(IllegalArgumentException.class); 64 | expectedException.expectMessage("certificateFile '' does not exist"); 65 | 66 | createEventStore(certificateFile); 67 | 68 | fail("Exception expected!"); 69 | } 70 | 71 | private void testSuccessfulConnection(final File certificateFile) throws InterruptedException { 72 | EventStore eventstore = createEventStore(certificateFile); 73 | 74 | CountDownLatch signal = new CountDownLatch(1); 75 | eventstore.addListener(onEvent(ClientConnected.class, e -> signal.countDown())); 76 | 77 | eventstore.connect(); 78 | 79 | assertSignal(signal); 80 | 81 | eventstore.shutdown(); 82 | } 83 | 84 | private static EventStore createEventStore(final File certificateFile) { 85 | return newEventStoreBuilder().useSslConnection(certificateFile).build(); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/subscription/VolatileSubscriptionOperation.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc.subscription; 2 | 3 | import com.github.msemys.esjc.ResolvedEvent; 4 | import com.github.msemys.esjc.Subscription; 5 | import com.github.msemys.esjc.SubscriptionListener; 6 | import com.github.msemys.esjc.UserCredentials; 7 | import com.github.msemys.esjc.operation.InspectionDecision; 8 | import com.github.msemys.esjc.operation.InspectionResult; 9 | import com.github.msemys.esjc.proto.EventStoreClientMessages.StreamEventAppeared; 10 | import com.github.msemys.esjc.proto.EventStoreClientMessages.SubscribeToStream; 11 | import com.github.msemys.esjc.proto.EventStoreClientMessages.SubscriptionConfirmation; 12 | import com.github.msemys.esjc.tcp.TcpCommand; 13 | import com.github.msemys.esjc.tcp.TcpPackage; 14 | import com.google.protobuf.MessageLite; 15 | import io.netty.channel.Channel; 16 | 17 | import java.util.concurrent.CompletableFuture; 18 | import java.util.concurrent.Executor; 19 | import java.util.function.Supplier; 20 | 21 | public class VolatileSubscriptionOperation extends AbstractSubscriptionOperation { 22 | 23 | @SuppressWarnings("unchecked") 24 | public VolatileSubscriptionOperation(CompletableFuture result, 25 | String streamId, 26 | boolean resolveLinkTos, 27 | UserCredentials userCredentials, 28 | SubscriptionListener listener, 29 | Supplier connectionSupplier, 30 | Executor executor) { 31 | super(result, TcpCommand.SubscribeToStream, streamId, resolveLinkTos, userCredentials, listener, connectionSupplier, executor); 32 | } 33 | 34 | @Override 35 | protected MessageLite createSubscribeMessage() { 36 | return SubscribeToStream.newBuilder() 37 | .setEventStreamId(streamId) 38 | .setResolveLinkTos(resolveLinkTos) 39 | .build(); 40 | } 41 | 42 | @Override 43 | protected VolatileSubscription createSubscription(long lastCommitPosition, Long lastEventNumber) { 44 | return new VolatileSubscription(this, streamId, lastCommitPosition, lastEventNumber); 45 | } 46 | 47 | @Override 48 | protected boolean inspect(TcpPackage tcpPackage, InspectionResult.Builder builder) { 49 | switch (tcpPackage.command) { 50 | case SubscriptionConfirmation: 51 | SubscriptionConfirmation confirmation = newInstance(SubscriptionConfirmation.getDefaultInstance(), tcpPackage.data); 52 | confirmSubscription(confirmation.getLastCommitPosition(), confirmation.hasLastEventNumber() ? confirmation.getLastEventNumber() : null); 53 | builder.decision(InspectionDecision.Subscribed).description("SubscriptionConfirmation"); 54 | return true; 55 | case StreamEventAppeared: 56 | StreamEventAppeared streamEventAppeared = newInstance(StreamEventAppeared.getDefaultInstance(), tcpPackage.data); 57 | eventAppeared(new ResolvedEvent(streamEventAppeared.getEvent())); 58 | builder.decision(InspectionDecision.DoNothing).description("StreamEventAppeared"); 59 | return true; 60 | default: 61 | return false; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/StreamAclJsonAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import com.google.gson.TypeAdapter; 4 | import com.google.gson.stream.JsonReader; 5 | import com.google.gson.stream.JsonToken; 6 | import com.google.gson.stream.JsonWriter; 7 | 8 | import java.io.IOException; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | import static java.util.Collections.singletonList; 13 | 14 | public class StreamAclJsonAdapter extends TypeAdapter { 15 | private static final String ACL_READ = "$r"; 16 | private static final String ACL_WRITE = "$w"; 17 | private static final String ACL_DELETE = "$d"; 18 | private static final String ACL_META_READ = "$mr"; 19 | private static final String ACL_META_WRITE = "$mw"; 20 | 21 | @Override 22 | public void write(JsonWriter writer, StreamAcl value) throws IOException { 23 | writer.beginObject(); 24 | writeRoles(writer, ACL_READ, value.readRoles); 25 | writeRoles(writer, ACL_WRITE, value.writeRoles); 26 | writeRoles(writer, ACL_DELETE, value.deleteRoles); 27 | writeRoles(writer, ACL_META_READ, value.metaReadRoles); 28 | writeRoles(writer, ACL_META_WRITE, value.metaWriteRoles); 29 | writer.endObject(); 30 | } 31 | 32 | @Override 33 | public StreamAcl read(JsonReader reader) throws IOException { 34 | StreamAcl.Builder builder = StreamAcl.newBuilder(); 35 | 36 | reader.beginObject(); 37 | 38 | while (reader.peek() != JsonToken.END_OBJECT && reader.hasNext()) { 39 | String name = reader.nextName(); 40 | switch (name) { 41 | case ACL_READ: 42 | builder.readRoles(readRoles(reader)); 43 | break; 44 | case ACL_WRITE: 45 | builder.writeRoles(readRoles(reader)); 46 | break; 47 | case ACL_DELETE: 48 | builder.deleteRoles(readRoles(reader)); 49 | break; 50 | case ACL_META_READ: 51 | builder.metaReadRoles(readRoles(reader)); 52 | break; 53 | case ACL_META_WRITE: 54 | builder.metaWriteRoles(readRoles(reader)); 55 | break; 56 | } 57 | } 58 | 59 | reader.endObject(); 60 | 61 | return builder.build(); 62 | } 63 | 64 | private static void writeRoles(JsonWriter writer, String name, List roles) throws IOException { 65 | if (roles != null) { 66 | writer.name(name); 67 | if (roles.size() == 1) { 68 | writer.value(roles.get(0)); 69 | } else { 70 | writer.beginArray(); 71 | for (String role : roles) { 72 | writer.value(role); 73 | } 74 | writer.endArray(); 75 | } 76 | } 77 | } 78 | 79 | private static List readRoles(JsonReader reader) throws IOException { 80 | if (reader.peek() == JsonToken.STRING) { 81 | return singletonList(reader.nextString()); 82 | } else { 83 | List roles = new ArrayList<>(); 84 | 85 | reader.beginArray(); 86 | 87 | while (reader.peek() != JsonToken.END_ARRAY) { 88 | roles.add(reader.nextString()); 89 | } 90 | 91 | reader.endArray(); 92 | 93 | return roles; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/github/msemys/esjc/Position.java: -------------------------------------------------------------------------------- 1 | package com.github.msemys.esjc; 2 | 3 | import static com.github.msemys.esjc.util.Preconditions.checkArgument; 4 | 5 | /** 6 | * Structure referring to a potential logical record position in the Event Store transaction file. 7 | */ 8 | public class Position implements Comparable { 9 | 10 | /** 11 | * Position representing the start of the transaction file. 12 | */ 13 | public static final Position START = new Position(0, 0); 14 | 15 | /** 16 | * Position representing the end of the transaction file. 17 | */ 18 | public static final Position END = new Position(-1, -1); 19 | 20 | /** 21 | * The commit position of the record. 22 | */ 23 | public final long commitPosition; 24 | 25 | /** 26 | * The prepare position of the record. 27 | */ 28 | public final long preparePosition; 29 | 30 | /** 31 | * Creates a new instance with the specified commit and prepare positions. 32 | *

33 | * It is not guaranteed that the position is actually the start of a record in the transaction file. 34 | *

35 | * 36 | * @param commitPosition the commit position of the record. 37 | * @param preparePosition the prepare position of the record. 38 | */ 39 | public Position(long commitPosition, long preparePosition) { 40 | checkArgument(commitPosition >= preparePosition, "The commit position cannot be less than the prepare position"); 41 | 42 | this.commitPosition = commitPosition; 43 | this.preparePosition = preparePosition; 44 | } 45 | 46 | @Override 47 | public int compareTo(Position that) { 48 | if (this.commitPosition < that.commitPosition || 49 | (this.commitPosition == that.commitPosition && this.preparePosition < that.preparePosition)) { 50 | return -1; 51 | } else if (this.commitPosition > that.commitPosition || 52 | (this.commitPosition == that.commitPosition && this.preparePosition > that.preparePosition)) { 53 | return 1; 54 | } else if (this.commitPosition == that.commitPosition && this.preparePosition == that.preparePosition) { 55 | return 0; 56 | } else { 57 | throw new IllegalStateException(); 58 | } 59 | } 60 | 61 | @Override 62 | public boolean equals(Object o) { 63 | if (this == o) return true; 64 | if (o == null || getClass() != o.getClass()) return false; 65 | 66 | Position position = (Position) o; 67 | 68 | if (commitPosition != position.commitPosition) return false; 69 | return preparePosition == position.preparePosition; 70 | 71 | } 72 | 73 | @Override 74 | public int hashCode() { 75 | int result = (int) (commitPosition ^ (commitPosition >>> 32)); 76 | result = 31 * result + (int) (preparePosition ^ (preparePosition >>> 32)); 77 | return result; 78 | } 79 | 80 | @Override 81 | public String toString() { 82 | return String.format("%d/%d", commitPosition, preparePosition); 83 | } 84 | 85 | /** 86 | * Creates a new instance with the specified commit and prepare positions. 87 | *

88 | * It is not guaranteed that the position is actually the start of a record in the transaction file. 89 | *

90 | * 91 | * @param commitPosition the commit position of the record. 92 | * @param preparePosition the prepare position of the record. 93 | */ 94 | public static Position of(long commitPosition, long preparePosition) { 95 | return new Position(commitPosition, preparePosition); 96 | } 97 | 98 | } 99 | --------------------------------------------------------------------------------