diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index cc4fdc293d0..f3d88b1c2fa 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ba94df8451f..4a6ebceacd2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/license-report.md b/license-report.md index 2435f785e21..6a3b50ee814 100644 --- a/license-report.md +++ b/license-report.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine:spine-client:1.4.9` +# Dependencies of `io.spine:spine-client:1.4.10` ## Runtime 1. **Group:** com.google.android **Name:** annotations **Version:** 4.1.1.4 @@ -415,12 +415,12 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Feb 27 18:05:32 EET 2020** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Tue Mar 03 16:17:23 EET 2020** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-core:1.4.9` +# Dependencies of `io.spine:spine-core:1.4.10` ## Runtime 1. **Group:** com.google.code.findbugs **Name:** jsr305 **Version:** 3.0.2 @@ -791,12 +791,12 @@ This report was generated on **Thu Feb 27 18:05:32 EET 2020** using [Gradle-Lice The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Feb 27 18:05:33 EET 2020** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Tue Mar 03 16:17:24 EET 2020** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:spine-model-assembler:1.4.9` +# Dependencies of `io.spine.tools:spine-model-assembler:1.4.10` ## Runtime 1. **Group:** com.google.android **Name:** annotations **Version:** 4.1.1.4 @@ -1206,12 +1206,12 @@ This report was generated on **Thu Feb 27 18:05:33 EET 2020** using [Gradle-Lice The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Feb 27 18:05:34 EET 2020** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Tue Mar 03 16:17:24 EET 2020** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:spine-model-verifier:1.4.9` +# Dependencies of `io.spine.tools:spine-model-verifier:1.4.10` ## Runtime 1. **Group:** com.google.android **Name:** annotations **Version:** 4.1.1.4 @@ -1681,12 +1681,12 @@ This report was generated on **Thu Feb 27 18:05:34 EET 2020** using [Gradle-Lice The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Feb 27 18:05:35 EET 2020** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Tue Mar 03 16:17:25 EET 2020** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-server:1.4.9` +# Dependencies of `io.spine:spine-server:1.4.10` ## Runtime 1. **Group:** com.google.android **Name:** annotations **Version:** 4.1.1.4 @@ -2113,12 +2113,12 @@ This report was generated on **Thu Feb 27 18:05:35 EET 2020** using [Gradle-Lice The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Feb 27 18:05:36 EET 2020** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Tue Mar 03 16:17:25 EET 2020** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-testutil-client:1.4.9` +# Dependencies of `io.spine:spine-testutil-client:1.4.10` ## Runtime 1. **Group:** com.google.android **Name:** annotations **Version:** 4.1.1.4 @@ -2586,12 +2586,12 @@ This report was generated on **Thu Feb 27 18:05:36 EET 2020** using [Gradle-Lice The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Feb 27 18:05:38 EET 2020** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Tue Mar 03 16:17:28 EET 2020** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-testutil-core:1.4.9` +# Dependencies of `io.spine:spine-testutil-core:1.4.10` ## Runtime 1. **Group:** com.google.android **Name:** annotations **Version:** 4.1.1.4 @@ -3067,12 +3067,12 @@ This report was generated on **Thu Feb 27 18:05:38 EET 2020** using [Gradle-Lice The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Feb 27 18:05:40 EET 2020** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Tue Mar 03 16:17:29 EET 2020** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-testutil-server:1.4.9` +# Dependencies of `io.spine:spine-testutil-server:1.4.10` ## Runtime 1. **Group:** com.google.android **Name:** annotations **Version:** 4.1.1.4 @@ -3584,4 +3584,4 @@ This report was generated on **Thu Feb 27 18:05:40 EET 2020** using [Gradle-Lice The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Feb 27 18:05:49 EET 2020** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file +This report was generated on **Tue Mar 03 16:17:32 EET 2020** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9c82360ab45..0e9f7dccadb 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ all modules and does not describe the project structure per-subproject. io.spine spine-core-java -1.4.9 +1.4.10 2015 @@ -70,7 +70,7 @@ all modules and does not describe the project structure per-subproject. io.spine spine-base - 1.4.8 + 1.4.9 compile @@ -82,13 +82,13 @@ all modules and does not describe the project structure per-subproject. io.spine.tools spine-model-compiler - 1.4.8 + 1.4.9 compile io.spine.tools spine-plugin-base - 1.4.8 + 1.4.9 compile @@ -130,7 +130,7 @@ all modules and does not describe the project structure per-subproject. io.spine spine-testlib - 1.4.8 + 1.4.9 test @@ -142,13 +142,13 @@ all modules and does not describe the project structure per-subproject. io.spine.tools spine-mute-logging - 1.4.8 + 1.4.9 test io.spine.tools spine-plugin-testlib - 1.4.8 + 1.4.9 test @@ -210,12 +210,12 @@ all modules and does not describe the project structure per-subproject. io.spine.tools spine-javadoc-filter - 1.4.8 + 1.4.9 io.spine.tools spine-protoc-plugin - 1.4.8 + 1.4.9 net.sourceforge.pmd diff --git a/server/src/main/java/io/spine/server/entity/AbstractEntity.java b/server/src/main/java/io/spine/server/entity/AbstractEntity.java index b2d732854b3..42c87d48e75 100644 --- a/server/src/main/java/io/spine/server/entity/AbstractEntity.java +++ b/server/src/main/java/io/spine/server/entity/AbstractEntity.java @@ -22,6 +22,8 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; +import com.google.common.flogger.FluentLogger; +import com.google.errorprone.annotations.OverridingMethodsMustInvokeSuper; import com.google.errorprone.annotations.concurrent.LazyInit; import com.google.protobuf.Any; import com.google.protobuf.Message; @@ -34,17 +36,23 @@ import io.spine.server.entity.model.EntityClass; import io.spine.server.entity.rejection.CannotModifyArchivedEntity; import io.spine.server.entity.rejection.CannotModifyDeletedEntity; +import io.spine.server.log.HandlerLifecycle; +import io.spine.server.log.HandlerLog; +import io.spine.server.model.HandlerMethod; import io.spine.string.Stringifiers; import io.spine.validate.ConstraintViolation; import io.spine.validate.Validate; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.List; import java.util.Objects; import java.util.function.Function; +import java.util.logging.Level; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static io.spine.logging.Logging.loggerFor; import static io.spine.util.Exceptions.newIllegalArgumentException; import static io.spine.validate.Validate.checkValid; @@ -59,9 +67,9 @@ @SuppressWarnings({ "SynchronizeOnThis" /* This class uses double-check idiom for lazy init of some fields. See Effective Java 2nd Ed. Item #71. */, - "AbstractClassWithoutAbstractMethods", "ClassWithTooManyMethods"}) -public abstract class AbstractEntity implements Entity { +public abstract class AbstractEntity + implements Entity, HandlerLifecycle { /** * Lazily initialized reference to the model class of this entity. @@ -108,6 +116,8 @@ public abstract class AbstractEntity implements Entity */ private volatile boolean lifecycleFlagsChanged; + private @Nullable HandlerLog handlerLog; + /** * Creates a new instance with the zero version and cleared lifecycle flags. * @@ -508,6 +518,42 @@ public Timestamp whenModified() { return version.getTimestamp(); } + @OverridingMethodsMustInvokeSuper + @Override + public void beforeInvoke(HandlerMethod method) { + checkNotNull(method); + FluentLogger logger = loggerFor(getClass()); + this.handlerLog = new HandlerLog(logger, method); + } + + @OverridingMethodsMustInvokeSuper + @Override + public void afterInvoke(HandlerMethod method) { + this.handlerLog = null; + } + + /** + * Obtains a new fluent logging API at the given level. + * + *

If called from within a handler method, the resulting log will reference the handler + * method as the log site. Otherwise, equivalent to + * {@code Logging.loggerFor(getClass()).at(logLevel)}. + * + * @param logLevel + * the log level + * @return new fluent logging API + * @apiNote This method mirrors the declaration of + * {@link io.spine.server.log.LoggingEntity#at(Level)}. It is recommended to implement + * the {@link io.spine.server.log.LoggingEntity} interface and use the underscore + * logging methods instead of calling {@code at(..)} directly. + * @see io.spine.server.log.LoggingEntity + */ + public final FluentLogger.Api at(Level logLevel) { + return handlerLog != null + ? handlerLog.at(logLevel) + : loggerFor(getClass()).at(logLevel); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/server/src/main/java/io/spine/server/log/HandlerLifecycle.java b/server/src/main/java/io/spine/server/log/HandlerLifecycle.java new file mode 100644 index 00000000000..bfeec8efeb2 --- /dev/null +++ b/server/src/main/java/io/spine/server/log/HandlerLifecycle.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.server.log; + +import io.spine.server.model.HandlerMethod; + +/** + * Callbacks for a {@link HandlerMethod} invocation. + * + *

If the target of the method implements {@code HandlerLifecycle}, it is invoked alongside + * handler method. For example: + *

+ * class SignUpSubscriber extends AbstractEventSubscriber implements HandlerLifecycle {
+ *
+ *    {@literal @Subscribe}
+ *     void on(UserSignedUp event) {
+ *         // ...
+ *     }
+ *
+ *    {@literal @Override}
+ *     public void beforeInvoke(HandlerMethod method) {
+ *         // ...
+ *     }
+ *
+ *    {@literal @Override}
+ *     public void afterInvoke(HandlerMethod method) {
+ *         // ...
+ *     }
+ * }
+ * 
+ * + *

When a {@code UserSignedUp} event is dispatched to the {@code SignUpSubscriber}, + * the invocation order goes as follows: + *

    + *
  1. {@code beforeInvoke([instance representing on(UserSignedUp) method])}. + *
  2. {@code on(UserSignedUp)}. + *
  3. {@code afterInvoke([instance representing on(UserSignedUp) method])}. + *
+ */ +public interface HandlerLifecycle { + + /** + * A callback for a handler method invocation start. + * + *

The handler method is invoked immediately after this method. + */ + void beforeInvoke(HandlerMethod method); + + /** + * A callback for a handler method invocation end. + * + *

This method is invoked immediately after the handler method, even if it has thrown + * an exception. + */ + void afterInvoke(HandlerMethod method); +} diff --git a/server/src/main/java/io/spine/server/log/HandlerLog.java b/server/src/main/java/io/spine/server/log/HandlerLog.java new file mode 100644 index 00000000000..e48caea7017 --- /dev/null +++ b/server/src/main/java/io/spine/server/log/HandlerLog.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.server.log; + +import com.google.common.flogger.FluentLogger; +import com.google.common.flogger.LogSite; +import io.spine.server.model.HandlerMethod; + +import java.util.logging.Level; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A log for handler methods. + * + *

The log is set up with a {@link HandlerMethod} from which it should be accessed. By default, + * the log will include the given method as the logging {@linkplain LogSite site}. + */ +public final class HandlerLog { + + private final FluentLogger logger; + private final LogSite logSite; + + public HandlerLog(FluentLogger logger, HandlerMethod method) { + this.logger = checkNotNull(logger); + checkNotNull(method); + this.logSite = new HandlerMethodSite(method); + } + + /** + * Returns a fluent logging API appropriate for the specified log level. + * + *

By default, the log produced by this API will include the name of the handler method, as + * well as its parameter types. + */ + public FluentLogger.Api at(Level level) { + return logger.at(level) + .withInjectedLogSite(logSite); + } +} diff --git a/server/src/main/java/io/spine/server/log/HandlerMethodSite.java b/server/src/main/java/io/spine/server/log/HandlerMethodSite.java new file mode 100644 index 00000000000..fa9d0416e50 --- /dev/null +++ b/server/src/main/java/io/spine/server/log/HandlerMethodSite.java @@ -0,0 +1,98 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.server.log; + +import com.google.common.base.Objects; +import com.google.common.flogger.LogSite; +import com.google.errorprone.annotations.Immutable; +import io.spine.code.java.ClassName; +import io.spine.code.java.SimpleClassName; +import io.spine.server.model.HandlerMethod; + +import java.lang.reflect.Method; +import java.util.stream.Stream; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; +import static java.util.stream.Collectors.joining; + +/** + * A {@link LogSite} of a signal handler method. + * + *

The site provides the name of the class and the name of the method with its parameter types. + * The site never provides a line number or a file name. + */ +@Immutable +final class HandlerMethodSite extends LogSite { + + @SuppressWarnings("Immutable") + private final Method method; + + HandlerMethodSite(HandlerMethod method) { + super(); + checkNotNull(method); + this.method = method.rawMethod(); + } + + @Override + public String getClassName() { + return method.getDeclaringClass() + .getName(); + } + + @Override + public String getMethodName() { + String params = Stream.of(method.getParameterTypes()) + .map(Class::getSimpleName) + .collect(joining(", ")); + String methodName = format("%s(%s)", method.getName(), params); + return methodName; + } + + @Override + public int getLineNumber() { + return UNKNOWN_LINE; + } + + @Override + public String getFileName() { + SimpleClassName className = ClassName.of(method.getDeclaringClass()) + .topLevelClass(); + return className.value() + ".java"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof HandlerMethodSite)) { + return false; + } + HandlerMethodSite site = (HandlerMethodSite) o; + return Objects.equal(method, site.method); + } + + @Override + public int hashCode() { + return Objects.hashCode(method); + } +} diff --git a/server/src/main/java/io/spine/server/log/LoggingEntity.java b/server/src/main/java/io/spine/server/log/LoggingEntity.java new file mode 100644 index 00000000000..1b330fbd66d --- /dev/null +++ b/server/src/main/java/io/spine/server/log/LoggingEntity.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.server.log; + +import com.google.common.flogger.FluentLogger; +import io.spine.logging.Logging; + +import java.util.logging.Level; + +/** + * A {@link Logging} trait for entities. + * + *

When adding logs in an entity, use this interface over {@link Logging}. + */ +public interface LoggingEntity extends Logging { + + /** + * Creates a {@code FluentLogger.Api} with the given level. + * + * @param level + * the log level + * @return new fluent logging API + * @apiNote This method mirrors the declaration of + * {@link io.spine.server.entity.AbstractEntity#at(Level)}. When a concrete entity + * implements this interface, the underscore logging methods will have the same + * behaviour regarding the log site as does the {@code AbstractEntity.at(Level)} method. + */ + FluentLogger.Api at(Level level); + + @Override + default FluentLogger.Api _severe() { + return at(Level.SEVERE); + } + + @Override + default FluentLogger.Api _warn() { + return at(Level.WARNING); + } + + @Override + default FluentLogger.Api _info() { + return at(Level.INFO); + } + + @Override + default FluentLogger.Api _config() { + return at(Level.CONFIG); + } + + @Override + default FluentLogger.Api _fine() { + return at(Level.FINE); + } + + @Override + default FluentLogger.Api _finer() { + return at(Level.FINER); + } + + @Override + default FluentLogger.Api _finest() { + return at(Level.FINEST); + } +} diff --git a/server/src/main/java/io/spine/server/log/package-info.java b/server/src/main/java/io/spine/server/log/package-info.java new file mode 100644 index 00000000000..f3998d0f18c --- /dev/null +++ b/server/src/main/java/io/spine/server/log/package-info.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Contains utilities which work with server-side logs. + */ + +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.server.log; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/src/main/java/io/spine/server/model/AbstractHandlerMethod.java b/server/src/main/java/io/spine/server/model/AbstractHandlerMethod.java index 4645ea0a2e6..f5cd37b4189 100644 --- a/server/src/main/java/io/spine/server/model/AbstractHandlerMethod.java +++ b/server/src/main/java/io/spine/server/model/AbstractHandlerMethod.java @@ -29,6 +29,7 @@ import io.spine.core.Signal; import io.spine.server.dispatch.DispatchOutcome; import io.spine.server.dispatch.Success; +import io.spine.server.log.HandlerLifecycle; import io.spine.server.type.MessageEnvelope; import io.spine.type.MessageClass; import org.checkerframework.checker.nullness.qual.Nullable; @@ -253,10 +254,14 @@ public DispatchOutcome invoke(T target, E envelope) { DispatchOutcome.Builder outcome = DispatchOutcome .newBuilder() .setPropagatedSignal(signal); + HandlerLifecycle lifecycle = target instanceof HandlerLifecycle + ? (HandlerLifecycle) target + : null; + if (lifecycle != null) { + lifecycle.beforeInvoke(this); + } try { - Object[] arguments = parameterSpec.extractArguments(envelope); - Object rawOutput = method.invoke(target, arguments); - Success success = toSuccessfulOutcome(rawOutput, target, envelope); + Success success = doInvoke(target, envelope); outcome.setSuccess(success); } catch (IllegalOutcomeException e) { Error error = fromThrowable(e); @@ -265,9 +270,7 @@ public DispatchOutcome invoke(T target, E envelope) { Throwable cause = e.getCause(); checkNotNull(cause); if (cause instanceof ThrowableMessage) { - ThrowableMessage throwable = (ThrowableMessage) cause; - Optional maybeSuccess = handleRejection(throwable, target, envelope); - Success success = maybeSuccess.orElseThrow(this::cannotThrowRejections); + Success success = asRejection(target, envelope, cause); outcome.setSuccess(success); } else { Error error = causeOf(cause); @@ -275,10 +278,27 @@ public DispatchOutcome invoke(T target, E envelope) { } } catch (IllegalArgumentException | IllegalAccessException e) { throw illegalStateWithCauseOf(e); + } finally { + if (lifecycle != null) { + lifecycle.afterInvoke(this); + } } return outcome.build(); } + private Success doInvoke(T target, E envelope) + throws IllegalAccessException, InvocationTargetException { + Object[] arguments = parameterSpec.extractArguments(envelope); + Object rawOutput = method.invoke(target, arguments); + return toSuccessfulOutcome(rawOutput, target, envelope); + } + + private Success asRejection(T target, E envelope, Throwable cause) { + ThrowableMessage throwable = (ThrowableMessage) cause; + Optional maybeSuccess = handleRejection(throwable, target, envelope); + return maybeSuccess.orElseThrow(this::cannotThrowRejections); + } + private RuntimeException cannotThrowRejections() { String errorMessage = format("`%s` may not throw rejections.", this); return new IllegalOutcomeException(errorMessage); diff --git a/server/src/test/java/io/spine/server/log/LoggingEntityTest.java b/server/src/test/java/io/spine/server/log/LoggingEntityTest.java new file mode 100644 index 00000000000..95e8e81cc22 --- /dev/null +++ b/server/src/test/java/io/spine/server/log/LoggingEntityTest.java @@ -0,0 +1,273 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.server.log; + +import com.google.common.flogger.FluentLogger; +import com.google.common.flogger.LoggerConfig; +import com.google.common.testing.TestLogHandler; +import io.spine.core.UserId; +import io.spine.logging.Logging; +import io.spine.server.DefaultRepository; +import io.spine.server.log.given.Books; +import io.spine.server.log.given.CardAggregate; +import io.spine.testing.core.given.GivenUserId; +import io.spine.testing.logging.LogRecordSubject; +import io.spine.testing.logging.MuteLogging; +import io.spine.testing.server.blackbox.BlackBoxBoundedContext; +import io.spine.testing.server.blackbox.SingleTenantBlackBoxContext; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.function.Function; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import static com.google.common.truth.Truth.assertThat; +import static io.spine.base.Identifier.newUuid; +import static io.spine.server.log.given.Books.THE_HOBBIT; +import static io.spine.testing.logging.LogTruth.assertThat; +import static java.util.logging.Level.ALL; +import static java.util.logging.Level.CONFIG; +import static java.util.logging.Level.FINE; +import static java.util.logging.Level.FINER; +import static java.util.logging.Level.FINEST; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.SEVERE; +import static java.util.logging.Level.WARNING; + +@MuteLogging +@DisplayName("`LoggingEntity` should") +class LoggingEntityTest { + + private FluentLogger logger; + private TestLogHandler handler; + private Handler[] defaultHandlers; + private @Nullable Level defaultLevel; + + @BeforeEach + void setUpLog() { + logger = Logging.loggerFor(CardAggregate.class); + handler = new TestLogHandler(); + LoggerConfig config = LoggerConfig.of(logger); + defaultHandlers = config.getHandlers(); + for (Handler defaultHandler : defaultHandlers) { + config.removeHandler(defaultHandler); + } + config.addHandler(handler); + defaultLevel = config.getLevel(); + config.setLevel(ALL); + } + + @AfterEach + void resetLog() { + LoggerConfig config = LoggerConfig.of(logger); + config.removeHandler(handler); + handler.close(); + for (Handler defaultHandler : defaultHandlers) { + config.addHandler(defaultHandler); + } + config.setLevel(defaultLevel); + } + + @Test + @DisplayName("log handler method name and parameters") + void includeSignalName() { + UserId user = GivenUserId.generated(); + BorrowBooks command = borrowBooks(user); + context().receivesCommand(command); + List records = handler.getStoredLogRecords(); + + assertThat(records) + .hasSize(2); + + LogRecordSubject assertFirstLog = assertThat(records.get(0)); + assertFirstLog.hasLevelThat() + .isEqualTo(FINE); + assertFirstLog.hasMessageThat() + .contains(Books.implementingDdd() + .getTitle()); + assertFirstLog.hasMethodNameThat() + .contains(command.getClass() + .getSimpleName()); + + LogRecordSubject assertSecondLog = assertThat(records.get(1)); + assertSecondLog.hasLevelThat() + .isEqualTo(FINE); + assertSecondLog.hasMessageThat() + .contains(Books.domainDrivenDesign() + .getTitle()); + assertSecondLog.hasMethodNameThat() + .contains(command.getClass() + .getSimpleName()); + } + + @Test + @DisplayName("log handler class name") + void includeClassName() { + UserId user = GivenUserId.generated(); + BorrowBooks command = borrowBooks(user); + context().receivesCommand(command); + List records = handler.getStoredLogRecords(); + + assertThat(records) + .hasSize(2); + for (LogRecord record : records) { + assertThat(record).hasClassNameThat() + .isEqualTo(CardAggregate.class.getName()); + } + } + + @Test + @DisplayName("pass the throwable unchanged") + void withCause() { + UserId user = GivenUserId.generated(); + ReturnBook command = ReturnBook + .newBuilder() + .setCard(cardId(user)) + .setBook(THE_HOBBIT) + .vBuild(); + context().receivesCommand(command); + List records = handler.getStoredLogRecords(); + assertThat(records) + .hasSize(1); + LogRecord record = records.get(0); + LogRecordSubject assertRecord = assertThat(record); + assertRecord.isError(); + assertRecord.hasThrowableThat() + .isInstanceOf(UnknownBook.class); + } + + private static SingleTenantBlackBoxContext context() { + return BlackBoxBoundedContext + .singleTenant() + .with(DefaultRepository.of(CardAggregate.class)); + } + + private static BorrowBooks borrowBooks(UserId reader) { + LibraryCardId id = cardId(reader); + BorrowBooks command = BorrowBooks + .newBuilder() + .setCard(id) + .addBookId(Books.BIG_RED_BOOK) + .addBookId(Books.BIG_BLUE_BOOK) + .vBuild(); + return command; + } + + private static LibraryCardId cardId(UserId reader) { + return LibraryCardId + .newBuilder() + .setReader(reader) + .build(); + } + + @Nested + @DisplayName("support method") + class Support { + + private String message; + + @BeforeEach + void randomizeMessage() { + message = newUuid(); + } + + @Test + @DisplayName("_severe") + void severe() { + testLevel(Logging::_severe, SEVERE); + } + + @Test + @DisplayName("_warn") + void warn() { + testLevel(Logging::_warn, WARNING); + } + + @Test + @DisplayName("_info") + void info() { + testLevel(Logging::_info, INFO); + } + + @Test + @DisplayName("_config") + void config() { + testLevel(Logging::_config, CONFIG); + } + + @Test + @DisplayName("_fine") + void fine() { + testLevel(Logging::_fine, FINE); + } + + @Test + @DisplayName("_finer") + void finer() { + testLevel(Logging::_finer, FINER); + } + + @Test + @DisplayName("_finest") + void finest() { + testLevel(Logging::_finest, FINEST); + } + + @Test + @DisplayName("_error") + void error() { + testLevel(Logging::_error, Logging.errorLevel()); + } + + @Test + @DisplayName("_debug") + void debug() { + testLevel(Logging::_debug, Logging.debugLevel()); + } + + @Test + @DisplayName("_trace") + void trace() { + testLevel(Logging::_trace, FINEST); + } + + private void testLevel(Function underscoreFunc, + Level expectedLevel) { + CardAggregate aggregate = new CardAggregate(); + underscoreFunc.apply(aggregate).log(message); + List records = handler.getStoredLogRecords(); + assertThat(records) + .hasSize(1); + LogRecordSubject assertLog = assertThat(records.get(0)); + assertLog.hasLevelThat() + .isEqualTo(expectedLevel); + assertLog.hasMessageThat() + .isEqualTo(message); + } + } +} diff --git a/server/src/test/java/io/spine/server/log/given/Books.java b/server/src/test/java/io/spine/server/log/given/Books.java new file mode 100644 index 00000000000..58a2a9294f1 --- /dev/null +++ b/server/src/test/java/io/spine/server/log/given/Books.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.server.log.given; + +import io.spine.people.PersonName; +import io.spine.server.log.Book; +import io.spine.server.log.Isbn; + +/** + * A test factory for instances of {@code Book} and {@code Isbn}. + */ +public final class Books { + + public static final Isbn BIG_BLUE_BOOK = isbn("978-0321125217"); + public static final Isbn BIG_RED_BOOK = isbn("978-0321834577"); + public static final Isbn SMALL_GREEN_BOOK = isbn("978-0134434421"); + + public static final Isbn THE_HOBBIT = isbn("978-0547928241"); + + private static final String PUBLISHER = "A-W"; + + /** + * Prevents the utility class instantiation. + */ + private Books() { + } + + public static Book domainDrivenDesign() { + return Book + .newBuilder() + .setIsbn(BIG_BLUE_BOOK) + .setTitle("Domain-Driven Design") + .addAuthor(PersonName.newBuilder().setGivenName("Eric")) + .setPublisher(PUBLISHER) + .vBuild(); + } + + public static Book implementingDdd() { + return Book + .newBuilder() + .setIsbn(BIG_BLUE_BOOK) + .setTitle("Implementing DDD") + .addAuthor(PersonName.newBuilder().setGivenName("Vaughn")) + .setPublisher(PUBLISHER) + .vBuild(); + } + + public static Book dddDistilled() { + return Book + .newBuilder() + .setIsbn(BIG_BLUE_BOOK) + .setTitle("DDD Distilled") + .addAuthor(PersonName.newBuilder().setGivenName("Vaughn")) + .setPublisher(PUBLISHER) + .vBuild(); + } + + private static Isbn isbn(String value) { + return Isbn + .newBuilder() + .setValue(value) + .build(); + } +} diff --git a/server/src/test/java/io/spine/server/log/given/CardAggregate.java b/server/src/test/java/io/spine/server/log/given/CardAggregate.java new file mode 100644 index 00000000000..22594b74545 --- /dev/null +++ b/server/src/test/java/io/spine/server/log/given/CardAggregate.java @@ -0,0 +1,124 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.server.log.given; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.spine.people.PersonName; +import io.spine.server.aggregate.Aggregate; +import io.spine.server.aggregate.Apply; +import io.spine.server.command.Assign; +import io.spine.server.log.Book; +import io.spine.server.log.BookReturned; +import io.spine.server.log.BooksBorrowed; +import io.spine.server.log.BorrowBooks; +import io.spine.server.log.Isbn; +import io.spine.server.log.LibraryCard; +import io.spine.server.log.LibraryCardId; +import io.spine.server.log.LoggingEntity; +import io.spine.server.log.ReturnBook; +import io.spine.server.log.UnknownBook; + +import java.util.ArrayList; +import java.util.List; + +import static io.spine.server.log.given.Books.BIG_BLUE_BOOK; +import static io.spine.server.log.given.Books.BIG_RED_BOOK; +import static io.spine.server.log.given.Books.SMALL_GREEN_BOOK; +import static io.spine.server.log.given.Books.dddDistilled; +import static io.spine.server.log.given.Books.domainDrivenDesign; +import static io.spine.server.log.given.Books.implementingDdd; + +public final class CardAggregate + extends Aggregate + implements LoggingEntity { + + private static final ImmutableMap knownBooks = ImmutableMap.of( + BIG_BLUE_BOOK, domainDrivenDesign(), + BIG_RED_BOOK, implementingDdd(), + SMALL_GREEN_BOOK, dddDistilled() + ); + + @Assign + BooksBorrowed handle(BorrowBooks command) throws UnknownBook { + BooksBorrowed.Builder event = BooksBorrowed + .newBuilder() + .setCard(id()); + List unknownBooks = new ArrayList<>(); + for (Isbn bookId : command.getBookIdList()) { + Book book = knownBooks.get(bookId); + if (book != null) { + event.addBook(book); + List authors = book.getAuthorList(); + PersonName firstAuthor = authors.get(0); + _fine().log("Adding to order: %s by %s %s", + book.getTitle(), + firstAuthor.getGivenName(), + firstAuthor.getFamilyName()); + } else { + _warn().log("Cannot lend an unknown book. ISBN: `%s`", bookId.getValue()); + unknownBooks.add(bookId); + } + } + if (!unknownBooks.isEmpty()) { + throw UnknownBook + .newBuilder() + .addAllBook(unknownBooks) + .build(); + } else { + return event.vBuild(); + } + } + + @Assign + BookReturned handle(ReturnBook command) throws UnknownBook { + Isbn isbn = command.getBook(); + Book book = knownBooks.get(isbn); + if (book == null) { + UnknownBook rejection = UnknownBook + .newBuilder() + .addAllBook(ImmutableList.of(isbn)) + .build(); + _error().withCause(rejection) + .log("Cannot return an unknown book. ISBN: `%s`", isbn.getValue()); + throw rejection; + } else { + return BookReturned + .newBuilder() + .setCard(id()) + .setBook(book) + .vBuild(); + } + } + + @Apply + private void on(BooksBorrowed event) { + builder().addAllBook(event.getBookList()); + } + + @Apply + private void on(BookReturned event) { + List list = builder().getBookList(); + Book book = event.getBook(); + int bookIndex = list.indexOf(book); + builder().removeBook(bookIndex); + } +} diff --git a/server/src/test/java/io/spine/server/log/given/package-info.java b/server/src/test/java/io/spine/server/log/given/package-info.java new file mode 100644 index 00000000000..dd802753176 --- /dev/null +++ b/server/src/test/java/io/spine/server/log/given/package-info.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Contains a small test domain and utilities for testing entity logs. + */ + +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.server.log.given; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/src/test/proto/spine/test/library/card.proto b/server/src/test/proto/spine/test/library/card.proto new file mode 100644 index 00000000000..d8b40285f05 --- /dev/null +++ b/server/src/test/proto/spine/test/library/card.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package spine.test.server.log; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io"; +option java_package = "io.spine.server.log"; +option java_outer_classname = "LibraryProto"; +option java_multiple_files = true; + +import "spine/people/person_name.proto"; +import "spine/time/time.proto"; +import "spine/core/user_id.proto"; + +message Isbn { + string value = 1 [(required) = true]; +} + +message Book { + + Isbn isbn = 1; + string title = 2; + repeated people.PersonName author = 3; + int32 year_when_published = 4; + string publisher = 5; +} + +message LibraryCardId { + + core.UserId reader = 1 [(required) = true]; +} + +message LibraryCard { + option (entity).kind = AGGREGATE; + + LibraryCardId id = 1; + + repeated Book book = 2; +} diff --git a/server/src/test/proto/spine/test/library/commands.proto b/server/src/test/proto/spine/test/library/commands.proto new file mode 100644 index 00000000000..240948da4c6 --- /dev/null +++ b/server/src/test/proto/spine/test/library/commands.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package spine.test.server.log; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io"; +option java_package = "io.spine.server.log"; +option java_outer_classname = "LibraryCommandsProto"; +option java_multiple_files = true; + +import "spine/test/library/card.proto"; + +message BorrowBooks { + LibraryCardId card = 1 [(required) = true, (validate) = true]; + repeated Isbn book_id = 2 [(required) = true, (validate) = true]; +} + +message ReturnBook { + LibraryCardId card = 1 [(required) = true, (validate) = true]; + Isbn book = 2 [(required) = true, (validate) = true]; +} diff --git a/server/src/test/proto/spine/test/library/events.proto b/server/src/test/proto/spine/test/library/events.proto new file mode 100644 index 00000000000..83b80f70d72 --- /dev/null +++ b/server/src/test/proto/spine/test/library/events.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package spine.test.server.log; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io"; +option java_package = "io.spine.server.log"; +option java_outer_classname = "LibraryEventsProto"; +option java_multiple_files = true; + +import "spine/test/library/card.proto"; + +message BooksBorrowed { + LibraryCardId card = 1; + repeated Book book = 2 [(required) = true]; +} + +message BookReturned { + LibraryCardId card = 1; + Book book = 2 [(required) = true]; +} diff --git a/server/src/test/proto/spine/test/library/rejections.proto b/server/src/test/proto/spine/test/library/rejections.proto new file mode 100644 index 00000000000..f2d29809539 --- /dev/null +++ b/server/src/test/proto/spine/test/library/rejections.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package spine.test.server.log; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io"; +option java_package = "io.spine.server.log"; +option java_outer_classname = "LibraryRejections"; + +import "spine/test/library/card.proto"; + +message UnknownBook { + repeated Isbn book = 1 [(required) = true]; +} diff --git a/testutil-server/src/main/java/io/spine/testing/server/blackbox/Actor.java b/testutil-server/src/main/java/io/spine/testing/server/blackbox/Actor.java new file mode 100644 index 00000000000..532e1a419e0 --- /dev/null +++ b/testutil-server/src/main/java/io/spine/testing/server/blackbox/Actor.java @@ -0,0 +1,113 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.testing.server.blackbox; + +import com.google.common.annotations.VisibleForTesting; +import io.spine.core.TenantId; +import io.spine.core.UserId; +import io.spine.testing.client.TestActorRequestFactory; +import io.spine.time.ZoneId; +import io.spine.time.ZoneOffset; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.spine.util.Preconditions2.checkNotEmptyOrBlank; +import static io.spine.validate.Validate.checkValid; + +/** + * A factory of test actor request factories. + */ +@VisibleForTesting +final class Actor { + + private static final Actor defaultActor = from(BlackBoxBoundedContext.class.getName()); + + private final UserId id; + private final ZoneId zoneId; + private final ZoneOffset zoneOffset; + + private Actor(UserId id, ZoneId zoneId, ZoneOffset zoneOffset) { + this.id = id; + this.zoneId = zoneId; + this.zoneOffset = zoneOffset; + } + + /** + * Obtains the default actor. + */ + static Actor defaultActor() { + return defaultActor; + } + + private static Actor from(String userId) { + checkNotEmptyOrBlank(userId); + UserId id = UserId + .newBuilder() + .setValue(userId) + .build(); + return from(id); + } + + /** + * Creates a new actor with the given actor ID and the default time zone. + */ + static Actor from(UserId userId) { + checkNotNull(userId); + checkValid(userId); + return new Actor(userId, + ZoneId.getDefaultInstance(), + ZoneOffset.getDefaultInstance()); + } + + /** + * Creates a new actor with the given time zone and the default actor ID. + */ + static Actor from(ZoneId zoneId, ZoneOffset zoneOffset) { + checkNotNull(zoneId); + checkNotNull(zoneOffset); + return new Actor(defaultActor.id, zoneId, zoneOffset); + } + + /** + * Creates a new actor with the given actor ID and time zone. + */ + static Actor from(UserId userId, ZoneId zoneId, ZoneOffset zoneOffset) { + checkNotNull(userId); + checkNotNull(zoneId); + checkNotNull(zoneOffset); + return new Actor(userId, zoneId, zoneOffset); + } + + /** + * Creates a new factory for requests of the single tenant. + */ + TestActorRequestFactory requests() { + return new TestActorRequestFactory(null, id, zoneOffset, zoneId); + } + + /** + * Creates a new factory for requests of the given tenant. + */ + TestActorRequestFactory requestsFor(TenantId tenant) { + checkNotNull(tenant); + checkValid(tenant); + return new TestActorRequestFactory(tenant, id, zoneOffset, zoneId); + } +} diff --git a/testutil-server/src/main/java/io/spine/testing/server/blackbox/BlackBoxBoundedContext.java b/testutil-server/src/main/java/io/spine/testing/server/blackbox/BlackBoxBoundedContext.java index 67839b215d2..31638a10aa8 100644 --- a/testutil-server/src/main/java/io/spine/testing/server/blackbox/BlackBoxBoundedContext.java +++ b/testutil-server/src/main/java/io/spine/testing/server/blackbox/BlackBoxBoundedContext.java @@ -40,6 +40,7 @@ import io.spine.core.BoundedContextName; import io.spine.core.Command; import io.spine.core.Event; +import io.spine.core.UserId; import io.spine.grpc.MemoizingObserver; import io.spine.logging.Logging; import io.spine.protobuf.AnyPacker; @@ -71,6 +72,8 @@ import io.spine.testing.server.blackbox.verify.state.VerifyState; import io.spine.testing.server.blackbox.verify.subscription.ToProtoSubjects; import io.spine.testing.server.entity.EntitySubject; +import io.spine.time.ZoneId; +import io.spine.time.ZoneOffset; import io.spine.type.TypeName; import org.checkerframework.checker.nullness.qual.Nullable; @@ -90,6 +93,7 @@ import static io.spine.core.BoundedContextNames.assumingTestsValue; import static io.spine.grpc.StreamObservers.memoizingObserver; import static io.spine.server.entity.model.EntityClass.stateClassOf; +import static io.spine.testing.server.blackbox.Actor.defaultActor; import static io.spine.util.Exceptions.illegalStateWithCauseOf; import static java.util.Collections.singletonList; import static java.util.Collections.synchronizedSet; @@ -162,6 +166,8 @@ public abstract class BlackBoxBoundedContext private final Map, Repository> repositories; + private Actor actor; + @SuppressWarnings("ThisEscapedInObjectConstruction") // to inject self as event dispatcher. protected BlackBoxBoundedContext(boolean multitenant, EventEnricher enricher, @@ -185,6 +191,7 @@ protected BlackBoxBoundedContext(boolean multitenant, this.repositories = newHashMap(); this.context.registerEventDispatcher(this); this.context.registerEventDispatcher(DiagnosticLog.instance()); + this.actor = defaultActor(); } /** @@ -323,6 +330,38 @@ public BoundedContextName name() { return context.name(); } + /** + * Sets the given {@link UserId} as the actor ID for the requests produced by this context. + */ + public final T withActor(UserId user) { + this.actor = Actor.from(user); + return thisRef(); + } + + /** + * Sets the given time zone parameters for the actor requests produced by this context. + */ + public final T in(ZoneId zoneId, ZoneOffset zoneOffset) { + this.actor = Actor.from(zoneId, zoneOffset); + return thisRef(); + } + + /** + * Sets the given actor ID and time zone parameters for the actor requests produced by this + * context. + */ + public final T withActorIn(UserId userId, ZoneId zoneId, ZoneOffset zoneOffset) { + this.actor = Actor.from(userId, zoneId, zoneOffset); + return thisRef(); + } + + /** + * Obtains the current {@link Actor}. + */ + protected final Actor actor() { + return this.actor; + } + /** * Obtains set of type names of entities known to this Bounded Context. */ diff --git a/testutil-server/src/main/java/io/spine/testing/server/blackbox/DiagnosticLog.java b/testutil-server/src/main/java/io/spine/testing/server/blackbox/DiagnosticLog.java index 91241e27e61..1de167fb8d6 100644 --- a/testutil-server/src/main/java/io/spine/testing/server/blackbox/DiagnosticLog.java +++ b/testutil-server/src/main/java/io/spine/testing/server/blackbox/DiagnosticLog.java @@ -20,7 +20,8 @@ package io.spine.testing.server.blackbox; -import com.google.common.flogger.FluentLogger; +import com.google.common.flogger.FluentLogger.Api; +import com.google.common.flogger.StackSize; import com.google.errorprone.annotations.FormatMethod; import com.google.errorprone.annotations.FormatString; import io.spine.base.EventMessage; @@ -128,7 +129,9 @@ void on(AggregateHistoryCorrupted event) { @FormatMethod private void log(EventMessage event, @FormatString String errorMessage, Object... formatArgs) { String msg = format(errorMessage, formatArgs); - FluentLogger.Api severeLogger = logger().atSevere(); + Api severeLogger = logger() + .atSevere() + .withStackTrace(StackSize.NONE); boolean loggingEnabled = severeLogger.isEnabled(); if (loggingEnabled) { severeLogger.log(msg); diff --git a/testutil-server/src/main/java/io/spine/testing/server/blackbox/MultitenantBlackBoxContext.java b/testutil-server/src/main/java/io/spine/testing/server/blackbox/MultitenantBlackBoxContext.java index bd6b65fc854..85f76c10ab9 100644 --- a/testutil-server/src/main/java/io/spine/testing/server/blackbox/MultitenantBlackBoxContext.java +++ b/testutil-server/src/main/java/io/spine/testing/server/blackbox/MultitenantBlackBoxContext.java @@ -66,7 +66,7 @@ public MultitenantBlackBoxContext withTenant(TenantId tenant) { @Override protected TestActorRequestFactory requestFactory() { - return new TestActorRequestFactory(MultitenantBlackBoxContext.class, tenantId()); + return actor().requestsFor(tenantId()); } @Override diff --git a/testutil-server/src/main/java/io/spine/testing/server/blackbox/SingleTenantBlackBoxContext.java b/testutil-server/src/main/java/io/spine/testing/server/blackbox/SingleTenantBlackBoxContext.java index f9eafa23736..fcd0ebf84c4 100644 --- a/testutil-server/src/main/java/io/spine/testing/server/blackbox/SingleTenantBlackBoxContext.java +++ b/testutil-server/src/main/java/io/spine/testing/server/blackbox/SingleTenantBlackBoxContext.java @@ -34,9 +34,6 @@ public final class SingleTenantBlackBoxContext extends BlackBoxBoundedContext { - private final TestActorRequestFactory requestFactory = - new TestActorRequestFactory(SingleTenantBlackBoxContext.class); - SingleTenantBlackBoxContext(String name, EventEnricher enricher) { super(false, enricher, name); } @@ -53,6 +50,6 @@ protected ImmutableList select(EventCollector collector) { @Override protected TestActorRequestFactory requestFactory() { - return requestFactory; + return actor().requests(); } } diff --git a/testutil-server/src/test/java/io/spine/testing/server/blackbox/BlackBoxBoundedContextTest.java b/testutil-server/src/test/java/io/spine/testing/server/blackbox/BlackBoxBoundedContextTest.java index 12fc4bf79c2..cd53a697356 100644 --- a/testutil-server/src/test/java/io/spine/testing/server/blackbox/BlackBoxBoundedContextTest.java +++ b/testutil-server/src/test/java/io/spine/testing/server/blackbox/BlackBoxBoundedContextTest.java @@ -29,6 +29,7 @@ import io.spine.client.QueryFactory; import io.spine.client.Topic; import io.spine.client.TopicFactory; +import io.spine.core.ActorContext; import io.spine.core.Event; import io.spine.core.UserId; import io.spine.server.BoundedContextBuilder; @@ -41,10 +42,12 @@ import io.spine.server.event.EventEnricher; import io.spine.server.projection.ProjectionRepository; import io.spine.server.type.CommandClass; +import io.spine.testing.core.given.GivenUserId; import io.spine.testing.logging.MuteLogging; import io.spine.testing.server.BlackBoxId; import io.spine.testing.server.EventSubject; import io.spine.testing.server.VerifyingCounter; +import io.spine.testing.server.blackbox.command.BbAssignSelf; import io.spine.testing.server.blackbox.command.BbCreateProject; import io.spine.testing.server.blackbox.command.BbFinalizeProject; import io.spine.testing.server.blackbox.command.BbRegisterCommandDispatcher; @@ -67,6 +70,10 @@ import io.spine.testing.server.blackbox.given.RepositoryThrowingExceptionOnClose; import io.spine.testing.server.blackbox.rejection.Rejections; import io.spine.testing.server.entity.EntitySubject; +import io.spine.time.ZoneId; +import io.spine.time.ZoneIds; +import io.spine.time.ZoneOffset; +import io.spine.time.ZoneOffsets; import io.spine.type.TypeName; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -79,10 +86,12 @@ import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import static io.spine.protobuf.AnyPacker.unpack; import static io.spine.testing.core.given.GivenUserId.newUuid; import static io.spine.testing.server.blackbox.given.Given.addProjectAssignee; import static io.spine.testing.server.blackbox.given.Given.addTask; +import static io.spine.testing.server.blackbox.given.Given.assignSelf; import static io.spine.testing.server.blackbox.given.Given.createProject; import static io.spine.testing.server.blackbox.given.Given.createReport; import static io.spine.testing.server.blackbox.given.Given.createdProjectState; @@ -703,4 +712,84 @@ void commandMessages() { .size()); } } + + @Nested + @DisplayName("produce requests with the given actor") + class WithGivenActor { + + @Test + @DisplayName("ID") + void id() { + UserId actor = GivenUserId.of("my-actor"); + BbCreateProject createProject = createProject(); + BbProjectId id = createProject.getProjectId(); + BbAssignSelf assignSelf = assignSelf(id); + context.withActor(actor) + .receivesCommands(createProject, assignSelf) + .assertEntityWithState(BbProject.class, id) + .hasStateThat() + .comparingExpectedFieldsOnly() + .isEqualTo(BbProject + .newBuilder() + .setId(id) + .addAssignee(actor) + .buildPartial()); + } + + @Test + @DisplayName("time zone") + void timeZone() { + UserId actor = GivenUserId.of("my-other-actor"); + BbCreateProject createProject = createProject(); + BbProjectId id = createProject.getProjectId(); + ZoneId zoneId = ZoneIds.of("UTC+1"); + ZoneOffset zoneOffset = ZoneOffsets.ofHours(1); + context.withActorIn(actor, zoneId, zoneOffset) + .receivesCommand(createProject) + .assertEntityWithState(BbProject.class, id) + .exists(); + EventSubject events = context.assertEvents() + .withType(BbProjectCreated.class); + events.hasSize(1); + ActorContext context = events.actual() + .get(0) + .context() + .actorContext(); + assertThat(context) + .comparingExpectedFieldsOnly() + .isEqualTo(ActorContext + .newBuilder() + .setActor(actor) + .setZoneId(zoneId) + .setZoneOffset(zoneOffset) + .buildPartial()); + } + + @Test + @DisplayName("ID and time zone") + void idAndTimeZone() { + BbCreateProject createProject = createProject(); + BbProjectId id = createProject.getProjectId(); + ZoneId zoneId = ZoneIds.of("UTC-1"); + ZoneOffset zoneOffset = ZoneOffsets.ofHours(-1); + context.in(zoneId, zoneOffset) + .receivesCommand(createProject) + .assertEntityWithState(BbProject.class, id) + .exists(); + EventSubject events = context.assertEvents() + .withType(BbProjectCreated.class); + events.hasSize(1); + ActorContext context = events.actual() + .get(0) + .context() + .actorContext(); + assertThat(context) + .comparingExpectedFieldsOnly() + .isEqualTo(ActorContext + .newBuilder() + .setZoneId(zoneId) + .setZoneOffset(zoneOffset) + .buildPartial()); + } + } } diff --git a/testutil-server/src/test/java/io/spine/testing/server/blackbox/given/BbProjectAggregate.java b/testutil-server/src/test/java/io/spine/testing/server/blackbox/given/BbProjectAggregate.java index fc8b868e80d..8e2b0cf8a45 100644 --- a/testutil-server/src/test/java/io/spine/testing/server/blackbox/given/BbProjectAggregate.java +++ b/testutil-server/src/test/java/io/spine/testing/server/blackbox/given/BbProjectAggregate.java @@ -20,6 +20,7 @@ package io.spine.testing.server.blackbox.given; +import io.spine.core.CommandContext; import io.spine.core.UserId; import io.spine.server.aggregate.Aggregate; import io.spine.server.aggregate.Apply; @@ -30,6 +31,7 @@ import io.spine.testing.server.blackbox.BbTask; import io.spine.testing.server.blackbox.command.BbAddTask; import io.spine.testing.server.blackbox.command.BbAssignProject; +import io.spine.testing.server.blackbox.command.BbAssignSelf; import io.spine.testing.server.blackbox.command.BbCreateProject; import io.spine.testing.server.blackbox.command.BbStartProject; import io.spine.testing.server.blackbox.event.BbAssigneeAdded; @@ -101,6 +103,16 @@ BbAssigneeAdded handle(BbAssignProject command) { .build(); } + @Assign + BbAssigneeAdded handle(BbAssignSelf command, CommandContext context) { + UserId assignee = context.getActorContext().getActor(); + return BbAssigneeAdded + .newBuilder() + .setId(id()) + .setUserId(assignee) + .build(); + } + @React(external = true) Optional on(BbUserDeleted event) { List assignees = state().getAssigneeList(); diff --git a/testutil-server/src/test/java/io/spine/testing/server/blackbox/given/Given.java b/testutil-server/src/test/java/io/spine/testing/server/blackbox/given/Given.java index a7e57148e7e..a10b80bd624 100644 --- a/testutil-server/src/test/java/io/spine/testing/server/blackbox/given/Given.java +++ b/testutil-server/src/test/java/io/spine/testing/server/blackbox/given/Given.java @@ -30,6 +30,7 @@ import io.spine.testing.server.blackbox.BbTask; import io.spine.testing.server.blackbox.command.BbAddTask; import io.spine.testing.server.blackbox.command.BbAssignProject; +import io.spine.testing.server.blackbox.command.BbAssignSelf; import io.spine.testing.server.blackbox.command.BbCreateProject; import io.spine.testing.server.blackbox.command.BbCreateReport; import io.spine.testing.server.blackbox.command.BbFinalizeProject; @@ -129,6 +130,13 @@ public static BbCreateProject createProject(BbProjectId id) { .build(); } + public static BbAssignSelf assignSelf(BbProjectId id) { + return BbAssignSelf + .newBuilder() + .setProjectId(id) + .build(); + } + public static BbInitProject initProject(BbProjectId id, boolean scrum) { BbInitProject.Builder builder = BbInitProject .newBuilder() diff --git a/testutil-server/src/test/proto/spine/testing/server/blackbox/commands.proto b/testutil-server/src/test/proto/spine/testing/server/blackbox/commands.proto index 714b3a827b6..6a2df5dc589 100644 --- a/testutil-server/src/test/proto/spine/testing/server/blackbox/commands.proto +++ b/testutil-server/src/test/proto/spine/testing/server/blackbox/commands.proto @@ -51,6 +51,10 @@ message BbAssignTeam { repeated spine.core.UserId member = 2 [(required) = true]; } +message BbAssignSelf { + BbProjectId project_id = 1; +} + message BbAssignScrumMaster { BbProjectId project_id = 1; spine.core.UserId scrum_master = 2 [(required) = true]; diff --git a/version.gradle b/version.gradle index 517675af4a4..545519294b3 100644 --- a/version.gradle +++ b/version.gradle @@ -25,14 +25,14 @@ * as we want to manage the versions in a single source. */ -final def spineVersion = '1.4.9' +final def spineVersion = '1.4.10' ext { // The version of the modules in this project. versionToPublish = spineVersion // Depend on `base` for the general definitions and a model compiler. - spineBaseVersion = '1.4.8' + spineBaseVersion = '1.4.9' // Depend on `time` for `ZoneId`, `ZoneOffset` and other date/time types and utilities. spineTimeVersion = '1.4.7'