diff --git a/build.sh b/build.sh index da1d0bd..e326905 100755 --- a/build.sh +++ b/build.sh @@ -18,7 +18,7 @@ compile() { } format() { - mvn formatter:format + mvn spotless:apply errcheck $? } diff --git a/pom.xml b/pom.xml index 871bcbc..7b7cb94 100644 --- a/pom.xml +++ b/pom.xml @@ -157,9 +157,14 @@ - net.revelc.code.formatter - formatter-maven-plugin - 2.26.0 + com.diffplug.spotless + spotless-maven-plugin + 2.44.5 + + + + + diff --git a/src/main/java/com/github/sttk/errs/Exc.java b/src/main/java/com/github/sttk/errs/Exc.java index 9eb5ee9..9ad7efc 100644 --- a/src/main/java/com/github/sttk/errs/Exc.java +++ b/src/main/java/com/github/sttk/errs/Exc.java @@ -4,29 +4,29 @@ */ package com.github.sttk.errs; -import java.lang.management.ManagementFactory; +import java.io.IOException; +import java.io.InvalidObjectException; +import java.io.NotSerializableException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; -import java.io.IOException; -import java.io.NotSerializableException; -import java.io.InvalidObjectException; +import java.lang.management.ManagementFactory; import java.time.OffsetDateTime; -import java.util.List; import java.util.LinkedList; +import java.util.List; /** * Is the exception class with a reason. - *

- * This class has a field which indicates a reason for this exception. Typically the type of this field is - * {@link Record}. In this case, the class name of this record represents the reason, and the fields of the record hold - * the situation where the exception occurred. - *

- * Optionally, this exception class can notify its instance creation to pre-registered exception handlers. This - * notification feature can be enabled by specifying the system property {@code -Dgithub.sttk.errs.notify=true} when the - * JVM is started. - *

- * The example code of creating and throwing an excepton is as follows: + * + *

This class has a field which indicates a reason for this exception. Typically the type of this + * field is {@link Record}. In this case, the class name of this record represents the reason, and + * the fields of the record hold the situation where the exception occurred. + * + *

Optionally, this exception class can notify its instance creation to pre-registered exception + * handlers. This notification feature can be enabled by specifying the system property {@code + * -Dgithub.sttk.errs.notify=true} when the JVM is started. + * + *

The example code of creating and throwing an excepton is as follows: * *

{@code
  * public record FailToDoSomething(String name, int value) {
@@ -41,273 +41,261 @@
  */
 public final class Exc extends Exception {
 
-    /** The serial version UID. */
-    private static final long serialVersionUID = 260427082865587554L;
-
-    /** The reason for this exception. */
-    private transient Object reason;
+  /** The serial version UID. */
+  private static final long serialVersionUID = 260427082865587554L;
 
-    /** The stack trace for the location of occurrence. */
-    private StackTraceElement trace;
+  /** The reason for this exception. */
+  private transient Object reason;
 
-    /**
-     * Is the constructor which takes an object indicating the reason for this exception.
-     *
-     * @param reason
-     *            A reason for this exception.
-     */
-    public Exc(final Object reason) {
-        if (reason == null) {
-            throw new IllegalArgumentException("reason is null");
-        }
-        this.reason = reason;
+  /** The stack trace for the location of occurrence. */
+  private StackTraceElement trace;
 
-        this.trace = getStackTrace()[0];
-
-        notifyExc(this);
+  /**
+   * Is the constructor which takes an object indicating the reason for this exception.
+   *
+   * @param reason A reason for this exception.
+   */
+  public Exc(final Object reason) {
+    if (reason == null) {
+      throw new IllegalArgumentException("reason is null");
     }
-
-    /**
-     * Is the constructor which takes an object indicating the reason and {@link Throwable} object indicating the cause
-     * for this exception.
-     *
-     * @param reason
-     *            A reason for this exception.
-     * @param cause
-     *            A cause for this exception.
-     */
-    @SuppressWarnings("this-escape")
-    public Exc(final Object reason, final Throwable cause) {
-        super(cause);
-
-        if (reason == null) {
-            throw new IllegalArgumentException("reason is null");
-        }
-        this.reason = reason;
-
-        this.trace = getStackTrace()[0];
-
-        notifyExc(this);
+    this.reason = reason;
+
+    this.trace = getStackTrace()[0];
+
+    notifyExc(this);
+  }
+
+  /**
+   * Is the constructor which takes an object indicating the reason and {@link Throwable} object
+   * indicating the cause for this exception.
+   *
+   * @param reason A reason for this exception.
+   * @param cause A cause for this exception.
+   */
+  @SuppressWarnings("this-escape")
+  public Exc(final Object reason, final Throwable cause) {
+    super(cause);
+
+    if (reason == null) {
+      throw new IllegalArgumentException("reason is null");
     }
-
-    /**
-     * Gets the reason for this exception. The type of the reason.
-     *
-     * @return The reason for this exception.
-     */
-    public Object getReason() {
-        return this.reason;
+    this.reason = reason;
+
+    this.trace = getStackTrace()[0];
+
+    notifyExc(this);
+  }
+
+  /**
+   * Gets the reason for this exception. The type of the reason.
+   *
+   * @return The reason for this exception.
+   */
+  public Object getReason() {
+    return this.reason;
+  }
+
+  /**
+   * Returns the message of this exception, that is the reason.
+   *
+   * @return The message of this exception.
+   */
+  @Override
+  public String getMessage() {
+    return reason.toString();
+  }
+
+  /**
+   * Returns the detail message of this exception, that contains the reason, source file name, line
+   * number, and the cause if provided.
+   *
+   * @return The message of this exception.
+   */
+  @Override
+  public String toString() {
+    var buf = new StringBuilder(getClass().getName());
+    buf.append(" { reason = ")
+        .append(reason.getClass().getName())
+        .append(" ")
+        .append(reason.toString());
+    buf.append(", file = ").append(this.trace.getFileName());
+    buf.append(", line = ").append(this.trace.getLineNumber());
+    if (getCause() != null) {
+      buf.append(", cause = ").append(getCause().toString());
     }
-
-    /**
-     * Returns the message of this exception, that is the reason.
-     *
-     * @return The message of this exception.
-     */
-    @Override
-    public String getMessage() {
-        return reason.toString();
+    return buf.append(" }").toString();
+  }
+
+  /**
+   * Returns the name of the source file of this exception occurrance.
+   *
+   * 

This method can return null if this information is unavailable. + * + * @return The name of the source file of this error occurrence. + */ + public String getFile() { + return this.trace.getFileName(); + } + + /** + * Returns the line number of this exception occurrance in the source file. + * + *

This method can return a negative number if this information is unavailable. + * + * @return The line number of this exception occurrance in the source file. + */ + public int getLine() { + return this.trace.getLineNumber(); + } + + /** + * Creates a {@link RuntimeException} object for methods that cannot throw a {@link Exc}. + * + * @return A {@link RuntimeException} object. + */ + public RuntimeException toRuntimeException() { + return new RuntimeExc(this); + } + + /** + * Writes a serial data of this exception to a stream. + * + *

Since a reason object is not necessarily serializable, this method will throw a {@link + * NotSerializableException} if the {@code reason} field does not inherit {@link Serializable}. + * + * @param out An {@link ObjectOutputStream} to which data is written. + * @throws IOException if an I/O error occurs. + */ + private void writeObject(ObjectOutputStream out) throws IOException { + if (!(this.reason instanceof Serializable)) { + throw new NotSerializableException(this.reason.getClass().getName()); } - - /** - * Returns the detail message of this exception, that contains the reason, source file name, line number, and the - * cause if provided. - * - * @return The message of this exception. - */ - @Override - public String toString() { - var buf = new StringBuilder(getClass().getName()); - buf.append(" { reason = ").append(reason.getClass().getName()).append(" ").append(reason.toString()); - buf.append(", file = ").append(this.trace.getFileName()); - buf.append(", line = ").append(this.trace.getLineNumber()); - if (getCause() != null) { - buf.append(", cause = ").append(getCause().toString()); - } - return buf.append(" }").toString(); + out.defaultWriteObject(); + out.writeObject(this.reason); + } + + /** + * Reconstitutes the {@code Exc} instance from a stream and initialize the reason and cause + * properties when deserializing. If the reason by deserialization is null, this method throws + * {@link InvalidObjectException}. + * + * @param in An {@link ObjectInputStream} from which data is read. + * @throws IOException if an I/O error occurs. + * @throws ClassNotFoundException if a serialized class cannot be loaded. + */ + private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException { + in.defaultReadObject(); + this.reason = in.readObject(); + + if (this.reason == null) { + throw new InvalidObjectException("reason is null or invalid."); } + } - /** - * Returns the name of the source file of this exception occurrance. - *

- * This method can return null if this information is unavailable. - * - * @return The name of the source file of this error occurrence. - */ - public String getFile() { - return this.trace.getFileName(); - } + //// Notification //// - /** - * Returns the line number of this exception occurrance in the source file. - *

- * This method can return a negative number if this information is unavailable. - * - * @return The line number of this exception occurrance in the source file. - */ - public int getLine() { - return this.trace.getLineNumber(); - } + private static final boolean useNotification; - /** - * Creates a {@link RuntimeException} object for methods that cannot throw a {@link Exc}. - * - * @return A {@link RuntimeException} object. - */ - public RuntimeException toRuntimeException() { - return new RuntimeExc(this); + static { + boolean b = false; + for (String arg : ManagementFactory.getRuntimeMXBean().getInputArguments()) { + if ("-Dgithub.sttk.errs.notify=true".equals(arg)) { + b = true; + break; + } } - - /** - * Writes a serial data of this exception to a stream. - *

- * Since a reason object is not necessarily serializable, this method will throw a {@link NotSerializableException} - * if the {@code reason} field does not inherit {@link Serializable}. - * - * @param out - * An {@link ObjectOutputStream} to which data is written. - * - * @throws IOException - * if an I/O error occurs. - */ - private void writeObject(ObjectOutputStream out) throws IOException { - if (!(this.reason instanceof Serializable)) { - throw new NotSerializableException(this.reason.getClass().getName()); - } - out.defaultWriteObject(); - out.writeObject(this.reason); + useNotification = b; + } + + private static boolean isHandlersFixed = false; + private static final List syncExcHandlers = new LinkedList<>(); + private static final List asyncExcHandlers = new LinkedList<>(); + + /** + * Adds an {@link ExcHandler} object which is executed synchronously just after an {@link Exc} is + * created. Handlers added with this method are executed in the order of addition and stop if one + * of the handlers throws a {@link RuntimeException} or an {@link Error}. NOTE: This feature is + * enabled via the system property: {@code github.sttk.errs.notify=true} + * + * @param handler An {@link ExcHandler} object. + */ + public static void addSyncHandler(final ExcHandler handler) { + if (!useNotification) return; + if (isHandlersFixed) return; + syncExcHandlers.add(handler); + } + + /** + * Adds an {@link ExcHandler} object which is executed asynchronously just after an {@link Exc} is + * created. Handlers don't stop even if one of the handlers throw a {@link RuntimeException} or an + * {@link Error}. NOTE: This feature is enabled via the system property: {@code + * github.sttk.errs.notify=true} + * + * @param handler An {@link ExcHandler} object. + */ + public static void addAsyncHandler(final ExcHandler handler) { + if (!useNotification) return; + if (isHandlersFixed) return; + asyncExcHandlers.add(handler); + } + + /** + * Prevents further addition of {@link ExcHandler} objects to synchronous and asynchronous + * exception handler lists. Before this is called, no {@code Exc} is notified to the handlers. + * After this is called, no new handlers can be added, and {@code Exc}(s) is notified to the + * handlers. NOTE: This feature is enabled via the system property: {@code + * github.sttk.errs.notify=true} + */ + public static void fixHandlers() { + if (!useNotification) return; + if (isHandlersFixed) return; + isHandlersFixed = true; + } + + private static void notifyExc(Exc exc) { + if (!useNotification) return; + if (!isHandlersFixed) return; + + if (syncExcHandlers.isEmpty() && asyncExcHandlers.isEmpty()) { + return; } - /** - * Reconstitutes the {@code Exc} instance from a stream and initialize the reason and cause properties when - * deserializing. If the reason by deserialization is null, this method throws {@link InvalidObjectException}. - * - * @param in - * An {@link ObjectInputStream} from which data is read. - * - * @throws IOException - * if an I/O error occurs. - * @throws ClassNotFoundException - * if a serialized class cannot be loaded. - */ - private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException { - in.defaultReadObject(); - this.reason = in.readObject(); - - if (this.reason == null) { - throw new InvalidObjectException("reason is null or invalid."); - } - } - - //// Notification //// - - private static final boolean useNotification; + final var tm = OffsetDateTime.now(); - static { - boolean b = false; - for (String arg : ManagementFactory.getRuntimeMXBean().getInputArguments()) { - if ("-Dgithub.sttk.errs.notify=true".equals(arg)) { - b = true; - break; - } - } - useNotification = b; + for (var handler : syncExcHandlers) { + handler.handle(exc, tm); } - private static boolean isHandlersFixed = false; - private static final List syncExcHandlers = new LinkedList<>(); - private static final List asyncExcHandlers = new LinkedList<>(); - - /** - * Adds an {@link ExcHandler} object which is executed synchronously just after an {@link Exc} is created. Handlers - * added with this method are executed in the order of addition and stop if one of the handlers throws a - * {@link RuntimeException} or an {@link Error}. NOTE: This feature is enabled via the system property: - * {@code github.sttk.errs.notify=true} - * - * @param handler - * An {@link ExcHandler} object. - */ - public static void addSyncHandler(final ExcHandler handler) { - if (!useNotification) - return; - if (isHandlersFixed) - return; - syncExcHandlers.add(handler); - } - - /** - * Adds an {@link ExcHandler} object which is executed asynchronously just after an {@link Exc} is created. Handlers - * don't stop even if one of the handlers throw a {@link RuntimeException} or an {@link Error}. NOTE: This feature - * is enabled via the system property: {@code github.sttk.errs.notify=true} - * - * @param handler - * An {@link ExcHandler} object. - */ - public static void addAsyncHandler(final ExcHandler handler) { - if (!useNotification) - return; - if (isHandlersFixed) - return; - asyncExcHandlers.add(handler); - } - - /** - * Prevents further addition of {@link ExcHandler} objects to synchronous and asynchronous exception handler lists. - * Before this is called, no {@code Exc} is notified to the handlers. After this is called, no new handlers can be - * added, and {@code Exc}(s) is notified to the handlers. NOTE: This feature is enabled via the system property: - * {@code github.sttk.errs.notify=true} - */ - public static void fixHandlers() { - if (!useNotification) - return; - if (isHandlersFixed) - return; - isHandlersFixed = true; - } - - private static void notifyExc(Exc exc) { - if (!useNotification) - return; - if (!isHandlersFixed) - return; - - if (syncExcHandlers.isEmpty() && asyncExcHandlers.isEmpty()) { - return; - } - - final var tm = OffsetDateTime.now(); - - for (var handler : syncExcHandlers) { - handler.handle(exc, tm); - } - - for (var handler : asyncExcHandlers) { - Thread.ofVirtual().start(() -> { + for (var handler : asyncExcHandlers) { + Thread.ofVirtual() + .start( + () -> { handler.handle(exc, tm); - }); - } + }); } + } } final class RuntimeExc extends RuntimeException { - private static final long serialVersionUID = 4664405757902479929L; - - RuntimeExc(Exc exc) { - super(exc); - } - - @Override - public String getMessage() { - return getCause().getMessage(); - } - - @Override - public String toString() { - return getClass().getName() + ": " + getCause().toString(); - } - - @Override - public Throwable fillInStackTrace() { - return null; - } + private static final long serialVersionUID = 4664405757902479929L; + + RuntimeExc(Exc exc) { + super(exc); + } + + @Override + public String getMessage() { + return getCause().getMessage(); + } + + @Override + public String toString() { + return getClass().getName() + ": " + getCause().toString(); + } + + @Override + public Throwable fillInStackTrace() { + return null; + } } diff --git a/src/main/java/com/github/sttk/errs/ExcHandler.java b/src/main/java/com/github/sttk/errs/ExcHandler.java index e3e5278..8d7c3e8 100644 --- a/src/main/java/com/github/sttk/errs/ExcHandler.java +++ b/src/main/java/com/github/sttk/errs/ExcHandler.java @@ -6,19 +6,15 @@ import java.time.OffsetDateTime; -/** - * {@code ExcHandler} is a handler of an {@link Exc} object creation. - */ +/** {@code ExcHandler} is a handler of an {@link Exc} object creation. */ @FunctionalInterface public interface ExcHandler { - /** - * Handles an {@link Exc} object creation. - * - * @param exc - * The {@link Exc} object. - * @param tm - * The creation time of the {@link Exc} object. - */ - void handle(Exc exc, OffsetDateTime tm); + /** + * Handles an {@link Exc} object creation. + * + * @param exc The {@link Exc} object. + * @param tm The creation time of the {@link Exc} object. + */ + void handle(Exc exc, OffsetDateTime tm); } diff --git a/src/main/java/com/github/sttk/errs/package-info.java b/src/main/java/com/github/sttk/errs/package-info.java index 4e00300..31e64b3 100644 --- a/src/main/java/com/github/sttk/errs/package-info.java +++ b/src/main/java/com/github/sttk/errs/package-info.java @@ -7,8 +7,9 @@ /** * Provides classes for handling an exception with a reason. - *

- * This package contains the {@code Exc} class which has a record field indicates the reason for the exception. + * + *

This package contains the {@code Exc} class which has a record field indicates the reason for + * the exception. * * @version 0.1 */ diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index c6c6ade..40cdcbf 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -12,5 +12,6 @@ */ module com.github.sttk.errs { exports com.github.sttk.errs; + requires java.management; } diff --git a/src/test/java/com/github/sttk/errs/ExcHandlerTest.java b/src/test/java/com/github/sttk/errs/ExcHandlerTest.java index ac1ec98..a888606 100644 --- a/src/test/java/com/github/sttk/errs/ExcHandlerTest.java +++ b/src/test/java/com/github/sttk/errs/ExcHandlerTest.java @@ -1,140 +1,142 @@ package com.github.sttk.errs; +import static java.time.format.DateTimeFormatter.ISO_INSTANT; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.BeforeEach; -import static java.time.format.DateTimeFormatter.ISO_INSTANT; -import java.util.List; import java.util.LinkedList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class ExcHandlerTest { - private ExcHandlerTest() { - } - - @BeforeEach - void reset() throws Exception { - var f = Exc.class.getDeclaredField("isHandlersFixed"); - f.setAccessible(true); - f.setBoolean(null, false); - - f = Exc.class.getDeclaredField("syncExcHandlers"); - f.setAccessible(true); - var o = f.get(null); - var m = LinkedList.class.getMethod("clear"); - m.invoke(o); - - f = Exc.class.getDeclaredField("asyncExcHandlers"); - f.setAccessible(true); - o = f.get(null); - m = LinkedList.class.getMethod("clear"); - m.invoke(o); - } - - @SuppressWarnings("unchecked") - List getSyncExcHandlers() throws Exception { - var f = Exc.class.getDeclaredField("syncExcHandlers"); - f.setAccessible(true); - var o = f.get(null); - return (List) o; - } - - @SuppressWarnings("unchecked") - List getAsyncExcHandlers() throws Exception { - var f = Exc.class.getDeclaredField("asyncExcHandlers"); - f.setAccessible(true); - var o = f.get(null); - return (List) o; - } - - @Test - void should_add_sync_handlers_and_fix() throws Exception { - var handlers = getSyncExcHandlers(); - assertThat(handlers).isEmpty(); - - ExcHandler handler1 = (exc, tm) -> { - }; - Exc.addSyncHandler(handler1); - - handlers = getSyncExcHandlers(); - assertThat(handlers).containsExactly(handler1); - - ExcHandler handler2 = (exc, tm) -> { - }; - Exc.addSyncHandler(handler2); - - handlers = getSyncExcHandlers(); - assertThat(handlers).containsExactly(handler1, handler2); - - Exc.fixHandlers(); - - ExcHandler handler3 = (exc, tm) -> { - }; - Exc.addSyncHandler(handler3); - - handlers = getSyncExcHandlers(); - assertThat(handlers).containsExactly(handler1, handler2); - } - - @Test - void should_add_async_handlers_and_fix() throws Exception { - var handlers = getAsyncExcHandlers(); - assertThat(handlers).isEmpty(); - - ExcHandler handler1 = (exc, tm) -> { - }; - Exc.addAsyncHandler(handler1); - - handlers = getAsyncExcHandlers(); - assertThat(handlers).containsExactly(handler1); - - ExcHandler handler2 = (exc, tm) -> { - }; - Exc.addAsyncHandler(handler2); - - handlers = getAsyncExcHandlers(); - assertThat(handlers).containsExactly(handler1, handler2); - - Exc.fixHandlers(); - - ExcHandler handler3 = (exc, tm) -> { - }; - Exc.addAsyncHandler(handler3); - - handlers = getAsyncExcHandlers(); - assertThat(handlers).containsExactly(handler1, handler2); - } - - @Test - void should_notify_exception() throws Exception { - final List syncLogs = new LinkedList<>(); - final List asyncLogs = new LinkedList<>(); - - Exc.addSyncHandler((exc, tm) -> { - syncLogs.add(String.format("%s:%s(%d):%s", tm.format(ISO_INSTANT), exc.getFile(), exc.getLine(), - exc.getReason().toString())); + private ExcHandlerTest() {} + + @BeforeEach + void reset() throws Exception { + var f = Exc.class.getDeclaredField("isHandlersFixed"); + f.setAccessible(true); + f.setBoolean(null, false); + + f = Exc.class.getDeclaredField("syncExcHandlers"); + f.setAccessible(true); + var o = f.get(null); + var m = LinkedList.class.getMethod("clear"); + m.invoke(o); + + f = Exc.class.getDeclaredField("asyncExcHandlers"); + f.setAccessible(true); + o = f.get(null); + m = LinkedList.class.getMethod("clear"); + m.invoke(o); + } + + @SuppressWarnings("unchecked") + List getSyncExcHandlers() throws Exception { + var f = Exc.class.getDeclaredField("syncExcHandlers"); + f.setAccessible(true); + var o = f.get(null); + return (List) o; + } + + @SuppressWarnings("unchecked") + List getAsyncExcHandlers() throws Exception { + var f = Exc.class.getDeclaredField("asyncExcHandlers"); + f.setAccessible(true); + var o = f.get(null); + return (List) o; + } + + @Test + void should_add_sync_handlers_and_fix() throws Exception { + var handlers = getSyncExcHandlers(); + assertThat(handlers).isEmpty(); + + ExcHandler handler1 = (exc, tm) -> {}; + Exc.addSyncHandler(handler1); + + handlers = getSyncExcHandlers(); + assertThat(handlers).containsExactly(handler1); + + ExcHandler handler2 = (exc, tm) -> {}; + Exc.addSyncHandler(handler2); + + handlers = getSyncExcHandlers(); + assertThat(handlers).containsExactly(handler1, handler2); + + Exc.fixHandlers(); + + ExcHandler handler3 = (exc, tm) -> {}; + Exc.addSyncHandler(handler3); + + handlers = getSyncExcHandlers(); + assertThat(handlers).containsExactly(handler1, handler2); + } + + @Test + void should_add_async_handlers_and_fix() throws Exception { + var handlers = getAsyncExcHandlers(); + assertThat(handlers).isEmpty(); + + ExcHandler handler1 = (exc, tm) -> {}; + Exc.addAsyncHandler(handler1); + + handlers = getAsyncExcHandlers(); + assertThat(handlers).containsExactly(handler1); + + ExcHandler handler2 = (exc, tm) -> {}; + Exc.addAsyncHandler(handler2); + + handlers = getAsyncExcHandlers(); + assertThat(handlers).containsExactly(handler1, handler2); + + Exc.fixHandlers(); + + ExcHandler handler3 = (exc, tm) -> {}; + Exc.addAsyncHandler(handler3); + + handlers = getAsyncExcHandlers(); + assertThat(handlers).containsExactly(handler1, handler2); + } + + @Test + void should_notify_exception() throws Exception { + final List syncLogs = new LinkedList<>(); + final List asyncLogs = new LinkedList<>(); + + Exc.addSyncHandler( + (exc, tm) -> { + syncLogs.add( + String.format( + "%s:%s(%d):%s", + tm.format(ISO_INSTANT), + exc.getFile(), + exc.getLine(), + exc.getReason().toString())); }); - Exc.addAsyncHandler((exc, tm) -> { - asyncLogs.add(String.format("%s:%s(%d):%s", tm.format(ISO_INSTANT), exc.getFile(), exc.getLine(), - exc.getReason().toString())); + Exc.addAsyncHandler( + (exc, tm) -> { + asyncLogs.add( + String.format( + "%s:%s(%d):%s", + tm.format(ISO_INSTANT), + exc.getFile(), + exc.getLine(), + exc.getReason().toString())); }); - record FailToDoSomething(String name) { - } + record FailToDoSomething(String name) {} - new Exc(new FailToDoSomething("abc")); + new Exc(new FailToDoSomething("abc")); - assertThat(syncLogs).isEmpty(); - assertThat(asyncLogs).isEmpty(); + assertThat(syncLogs).isEmpty(); + assertThat(asyncLogs).isEmpty(); - Exc.fixHandlers(); + Exc.fixHandlers(); - new Exc(new FailToDoSomething("abc")); - assertThat(syncLogs.get(0)).endsWith(":ExcHandlerTest.java(134):FailToDoSomething[name=abc]"); + new Exc(new FailToDoSomething("abc")); + assertThat(syncLogs.get(0)).endsWith(":ExcHandlerTest.java(136):FailToDoSomething[name=abc]"); - Thread.sleep(100); - assertThat(asyncLogs.get(0)).endsWith(":ExcHandlerTest.java(134):FailToDoSomething[name=abc]"); - } + Thread.sleep(100); + assertThat(asyncLogs.get(0)).endsWith(":ExcHandlerTest.java(136):FailToDoSomething[name=abc]"); + } } diff --git a/src/test/java/com/github/sttk/errs/ExcTest.java b/src/test/java/com/github/sttk/errs/ExcTest.java index 6718740..468f890 100644 --- a/src/test/java/com/github/sttk/errs/ExcTest.java +++ b/src/test/java/com/github/sttk/errs/ExcTest.java @@ -2,352 +2,351 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.fail; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.NotSerializableException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; -import java.io.Serializable; -import java.io.NotSerializableException; -import java.io.InvalidObjectException; import java.io.PrintWriter; +import java.io.Serializable; import java.io.StringWriter; -import java.io.IOException; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; public class ExcTest { - private ExcTest() { - } + private ExcTest() {} - /// exception reasons /// + /// exception reasons /// - record IndexOutOfRange(String name, int index, int min, int max) { - } + record IndexOutOfRange(String name, int index, int min, int max) {} - record SerializableReason(String name, int index, int min, int max) implements Serializable { - } + record SerializableReason(String name, int index, int min, int max) implements Serializable {} - @Nested - class TestConstructor { - @Test - void with_Record_reason() { - var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); - var reason = IndexOutOfRange.class.cast(exc.getReason()); - assertThat(reason.name()).isEqualTo("data"); - assertThat(reason.index()).isEqualTo(4); - assertThat(reason.min()).isEqualTo(0); - assertThat(reason.max()).isEqualTo(3); - assertThat(exc.getCause()).isNull(); - - // exc.printStackTrace(); - } + @Nested + class TestConstructor { + @Test + void with_Record_reason() { + var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); + var reason = IndexOutOfRange.class.cast(exc.getReason()); + assertThat(reason.name()).isEqualTo("data"); + assertThat(reason.index()).isEqualTo(4); + assertThat(reason.min()).isEqualTo(0); + assertThat(reason.max()).isEqualTo(3); + assertThat(exc.getCause()).isNull(); - @Test - void with_enum_reason() { - enum Reasons { - FailToDoSomething, - } + // exc.printStackTrace(); + } - var exc = new Exc(Reasons.FailToDoSomething); - var reason = Reasons.class.cast(exc.getReason()); - assertThat(reason.name()).isEqualTo("FailToDoSomething"); - assertThat(exc.getCause()).isNull(); + @Test + void with_enum_reason() { + enum Reasons { + FailToDoSomething, + } - // exc.printStackTrace(); - } + var exc = new Exc(Reasons.FailToDoSomething); + var reason = Reasons.class.cast(exc.getReason()); + assertThat(reason.name()).isEqualTo("FailToDoSomething"); + assertThat(exc.getCause()).isNull(); - @Test - void with_String_reason() { - var exc = new Exc("FailToDoSomething"); - var reason = String.class.cast(exc.getReason()); - assertThat(reason).isEqualTo("FailToDoSomething"); - assertThat(exc.getCause()).isNull(); - } + // exc.printStackTrace(); + } - @Test - void with_reason_but_reason_is_null() { - try { - new Exc(null); - } catch (IllegalArgumentException e) { - assertThat(e.getMessage()).isEqualTo("reason is null"); - } - } + @Test + void with_String_reason() { + var exc = new Exc("FailToDoSomething"); + var reason = String.class.cast(exc.getReason()); + assertThat(reason).isEqualTo("FailToDoSomething"); + assertThat(exc.getCause()).isNull(); + } - @Test - void with_reason_and_cause() { - var cause = new IndexOutOfBoundsException(4); - var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3), cause); - var reason = IndexOutOfRange.class.cast(exc.getReason()); - assertThat(reason.name()).isEqualTo("data"); - assertThat(reason.index()).isEqualTo(4); - assertThat(reason.min()).isEqualTo(0); - assertThat(reason.max()).isEqualTo(3); - assertThat(exc.getCause()).isEqualTo(cause); - - // exc.printStackTrace(); - } + @Test + void with_reason_but_reason_is_null() { + try { + new Exc(null); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).isEqualTo("reason is null"); + } + } - @Test - void with_reason_and_cause_but_reason_is_null() { - var cause = new IndexOutOfBoundsException(4); - try { - new Exc(null, cause); - } catch (IllegalArgumentException e) { - assertThat(e.getMessage()).isEqualTo("reason is null"); - } - } + @Test + void with_reason_and_cause() { + var cause = new IndexOutOfBoundsException(4); + var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3), cause); + var reason = IndexOutOfRange.class.cast(exc.getReason()); + assertThat(reason.name()).isEqualTo("data"); + assertThat(reason.index()).isEqualTo(4); + assertThat(reason.min()).isEqualTo(0); + assertThat(reason.max()).isEqualTo(3); + assertThat(exc.getCause()).isEqualTo(cause); + + // exc.printStackTrace(); + } - @Test - void with_reason_and_cause_but_cause_is_null() { - var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3), null); - var reason = IndexOutOfRange.class.cast(exc.getReason()); - assertThat(reason.name()).isEqualTo("data"); - assertThat(reason.index()).isEqualTo(4); - assertThat(reason.min()).isEqualTo(0); - assertThat(reason.max()).isEqualTo(3); - assertThat(exc.getCause()).isNull(); - - // exc.printStackTrace(); - } + @Test + void with_reason_and_cause_but_reason_is_null() { + var cause = new IndexOutOfBoundsException(4); + try { + new Exc(null, cause); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).isEqualTo("reason is null"); + } } - @Nested - class TestThrow { - @Test - void identify_reason_with_instanceOf() { - var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); - if (exc.getReason() instanceof IndexOutOfRange reason) { - assertThat(reason.name()).isEqualTo("data"); - assertThat(reason.index()).isEqualTo(4); - assertThat(reason.min()).isEqualTo(0); - assertThat(reason.max()).isEqualTo(3); - } - } + @Test + void with_reason_and_cause_but_cause_is_null() { + var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3), null); + var reason = IndexOutOfRange.class.cast(exc.getReason()); + assertThat(reason.name()).isEqualTo("data"); + assertThat(reason.index()).isEqualTo(4); + assertThat(reason.min()).isEqualTo(0); + assertThat(reason.max()).isEqualTo(3); + assertThat(exc.getCause()).isNull(); + + // exc.printStackTrace(); + } + } + + @Nested + class TestThrow { + @Test + void identify_reason_with_instanceOf() { + var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); + if (exc.getReason() instanceof IndexOutOfRange reason) { + assertThat(reason.name()).isEqualTo("data"); + assertThat(reason.index()).isEqualTo(4); + assertThat(reason.min()).isEqualTo(0); + assertThat(reason.max()).isEqualTo(3); + } + } - @Test - void identify_Record_reason_with_switch_expression() { - var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); - switch (exc.getReason()) { - case IndexOutOfRange reason -> { - assertThat(reason.name()).isEqualTo("data"); - assertThat(reason.index()).isEqualTo(4); - assertThat(reason.min()).isEqualTo(0); - assertThat(reason.max()).isEqualTo(3); - } - default -> fail(); - } + @Test + void identify_Record_reason_with_switch_expression() { + var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); + switch (exc.getReason()) { + case IndexOutOfRange reason -> { + assertThat(reason.name()).isEqualTo("data"); + assertThat(reason.index()).isEqualTo(4); + assertThat(reason.min()).isEqualTo(0); + assertThat(reason.max()).isEqualTo(3); } + default -> fail(); + } + } - @Test - void identify_Enum_reason_with_switch_expression() { - enum Reasons { - FailToDoSomething, InvalidValue, - } - - var exc = new Exc(Reasons.FailToDoSomething); - - var s = switch (exc.getReason()) { - case Reasons enm -> switch (enm) { - case FailToDoSomething -> "fail to do something"; - case InvalidValue -> "invalid value"; + @Test + void identify_Enum_reason_with_switch_expression() { + enum Reasons { + FailToDoSomething, + InvalidValue, + } + + var exc = new Exc(Reasons.FailToDoSomething); + + var s = + switch (exc.getReason()) { + case Reasons enm -> + switch (enm) { + case FailToDoSomething -> "fail to do something"; + case InvalidValue -> "invalid value"; }; - default -> "unknown"; - }; - assertThat(s).isEqualTo("fail to do something"); - } + default -> "unknown"; + }; + assertThat(s).isEqualTo("fail to do something"); } - - @Nested - class TestGetter { - @Test - void getReason() { - var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); - assertThat(exc.getReason()).isInstanceOf(IndexOutOfRange.class); - - var reason = IndexOutOfRange.class.cast(exc.getReason()); - assertThat(reason.name()).isEqualTo("data"); - assertThat(reason.index()).isEqualTo(4); - assertThat(reason.min()).isEqualTo(0); - assertThat(reason.max()).isEqualTo(3); - } - - @Test - void getCause() { - var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); - assertThat(exc.getCause()).isNull(); - - var cause = new IndexOutOfBoundsException(4); - exc = new Exc(new IndexOutOfRange("data", 4, 0, 3), cause); - assertThat(exc.getCause()).isEqualTo(cause); - } - - @Test - void getFile() { - var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); - assertThat(exc.getFile()).isEqualTo("ExcTest.java"); - } - - @Test - void getLine() { - var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); - assertThat(exc.getLine()).isEqualTo(194); - } + } + + @Nested + class TestGetter { + @Test + void getReason() { + var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); + assertThat(exc.getReason()).isInstanceOf(IndexOutOfRange.class); + + var reason = IndexOutOfRange.class.cast(exc.getReason()); + assertThat(reason.name()).isEqualTo("data"); + assertThat(reason.index()).isEqualTo(4); + assertThat(reason.min()).isEqualTo(0); + assertThat(reason.max()).isEqualTo(3); } - @Nested - class TestGetMessage { - @Test - void with_cause() { - var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); - assertThat(exc.getMessage()).isEqualTo("IndexOutOfRange[name=data, index=4, min=0, max=3]"); - } + @Test + void getCause() { + var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); + assertThat(exc.getCause()).isNull(); - @Test - void with_no_cause() { - var cause = new IndexOutOfBoundsException(4); - var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3), cause); - assertThat(exc.getMessage()).isEqualTo("IndexOutOfRange[name=data, index=4, min=0, max=3]"); - } + var cause = new IndexOutOfBoundsException(4); + exc = new Exc(new IndexOutOfRange("data", 4, 0, 3), cause); + assertThat(exc.getCause()).isEqualTo(cause); } - @Nested - class TestToString { - @Test - void with_reason() { - var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); - assertThat(exc.toString()).isEqualTo( - "com.github.sttk.errs.Exc { reason = com.github.sttk.errs.ExcTest$IndexOutOfRange IndexOutOfRange[name=data, index=4, min=0, max=3], file = ExcTest.java, line = 219 }"); - } + @Test + void getFile() { + var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); + assertThat(exc.getFile()).isEqualTo("ExcTest.java"); + } - @Test - void with_reason_and_cause() { - var cause = new IndexOutOfBoundsException(4); - var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3), cause); - assertThat(exc.toString()).isEqualTo( - "com.github.sttk.errs.Exc { reason = com.github.sttk.errs.ExcTest$IndexOutOfRange IndexOutOfRange[name=data, index=4, min=0, max=3], file = ExcTest.java, line = 227, cause = java.lang.IndexOutOfBoundsException: Index out of range: 4 }"); - } + @Test + void getLine() { + var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); + assertThat(exc.getLine()).isEqualTo(191); + } + } + + @Nested + class TestGetMessage { + @Test + void with_cause() { + var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); + assertThat(exc.getMessage()).isEqualTo("IndexOutOfRange[name=data, index=4, min=0, max=3]"); } - @Nested - class TestToRuntimeException { - @Test - void getMessage() { - var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); - var rtExc = exc.toRuntimeException(); - assertThat(rtExc.getMessage()).isEqualTo("IndexOutOfRange[name=data, index=4, min=0, max=3]"); - } + @Test + void with_no_cause() { + var cause = new IndexOutOfBoundsException(4); + var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3), cause); + assertThat(exc.getMessage()).isEqualTo("IndexOutOfRange[name=data, index=4, min=0, max=3]"); + } + } + + @Nested + class TestToString { + @Test + void with_reason() { + var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); + assertThat(exc.toString()) + .isEqualTo( + "com.github.sttk.errs.Exc { reason = com.github.sttk.errs.ExcTest$IndexOutOfRange IndexOutOfRange[name=data, index=4, min=0, max=3], file = ExcTest.java, line = 216 }"); + } - @Test - void getCause() { - var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); - var rtExc = exc.toRuntimeException(); - assertThat(rtExc.getCause()).isEqualTo(exc); - } + @Test + void with_reason_and_cause() { + var cause = new IndexOutOfBoundsException(4); + var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3), cause); + assertThat(exc.toString()) + .isEqualTo( + "com.github.sttk.errs.Exc { reason = com.github.sttk.errs.ExcTest$IndexOutOfRange IndexOutOfRange[name=data, index=4, min=0, max=3], file = ExcTest.java, line = 225, cause = java.lang.IndexOutOfBoundsException: Index out of range: 4 }"); + } + } + + @Nested + class TestToRuntimeException { + @Test + void getMessage() { + var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); + var rtExc = exc.toRuntimeException(); + assertThat(rtExc.getMessage()).isEqualTo("IndexOutOfRange[name=data, index=4, min=0, max=3]"); + } - @Test - void printStackTrace() { - var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); - var rtExc = exc.toRuntimeException(); - - var swOfExc = new StringWriter(); - try (var pwOfExc = new PrintWriter(swOfExc)) { - exc.printStackTrace(pwOfExc); - } - var swOfRtExc = new StringWriter(); - try (var pwOfRtExc = new PrintWriter(swOfRtExc)) { - rtExc.printStackTrace(pwOfRtExc); - } - - var isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows"); - var prefix = "com.github.sttk.errs.RuntimeExc: " + exc.toString(); - if (isWindows) { - prefix += System.lineSeparator(); - } else { - prefix += System.lineSeparator(); - } - prefix += "Caused by: "; - - assertThat(swOfRtExc.toString()).isEqualTo(prefix + swOfExc.toString()); - - // rtExc.printStackTrace(); - } + @Test + void getCause() { + var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); + var rtExc = exc.toRuntimeException(); + assertThat(rtExc.getCause()).isEqualTo(exc); } - @Nested - class TestSerialize { - @Test - void reason_is_serializable_and_has_no_cause() throws Exception { - var bos = new ByteArrayOutputStream(); - var oos = new ObjectOutputStream(bos); - try (oos) { - var exc = new Exc(new SerializableReason("data", 4, 0, 3)); - oos.writeObject(exc); - } - - var bytes = bos.toByteArray(); - var ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); - try (ois) { - var obj = ois.readObject(); - assertThat(obj).isInstanceOf(Exc.class); - - var exc = Exc.class.cast(obj); - var cause = exc.getCause(); - assertThat(cause).isNull(); - - var robj = exc.getReason(); - assertThat(robj).isInstanceOf(SerializableReason.class); - var reason = SerializableReason.class.cast(robj); - assertThat(reason.name()).isEqualTo("data"); - assertThat(reason.index()).isEqualTo(4); - assertThat(reason.min()).isEqualTo(0); - assertThat(reason.max()).isEqualTo(3); - } - } + @Test + void printStackTrace() { + var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); + var rtExc = exc.toRuntimeException(); + + var swOfExc = new StringWriter(); + try (var pwOfExc = new PrintWriter(swOfExc)) { + exc.printStackTrace(pwOfExc); + } + var swOfRtExc = new StringWriter(); + try (var pwOfRtExc = new PrintWriter(swOfRtExc)) { + rtExc.printStackTrace(pwOfRtExc); + } + + var isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows"); + var prefix = "com.github.sttk.errs.RuntimeExc: " + exc.toString(); + if (isWindows) { + prefix += System.lineSeparator(); + } else { + prefix += System.lineSeparator(); + } + prefix += "Caused by: "; + + assertThat(swOfRtExc.toString()).isEqualTo(prefix + swOfExc.toString()); + + // rtExc.printStackTrace(); + } + } + + @Nested + class TestSerialize { + @Test + void reason_is_serializable_and_has_no_cause() throws Exception { + var bos = new ByteArrayOutputStream(); + var oos = new ObjectOutputStream(bos); + try (oos) { + var exc = new Exc(new SerializableReason("data", 4, 0, 3)); + oos.writeObject(exc); + } + + var bytes = bos.toByteArray(); + var ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); + try (ois) { + var obj = ois.readObject(); + assertThat(obj).isInstanceOf(Exc.class); + + var exc = Exc.class.cast(obj); + var cause = exc.getCause(); + assertThat(cause).isNull(); + + var robj = exc.getReason(); + assertThat(robj).isInstanceOf(SerializableReason.class); + var reason = SerializableReason.class.cast(robj); + assertThat(reason.name()).isEqualTo("data"); + assertThat(reason.index()).isEqualTo(4); + assertThat(reason.min()).isEqualTo(0); + assertThat(reason.max()).isEqualTo(3); + } + } - @Test - void reason_is_serializable_and_has_cause() throws Exception { - var bos = new ByteArrayOutputStream(); - var oos = new ObjectOutputStream(bos); - try (oos) { - var cause = new IndexOutOfBoundsException(4); - var exc = new Exc(new SerializableReason("data", 4, 0, 3), cause); - oos.writeObject(exc); - } - - var bytes = bos.toByteArray(); - var ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); - try (ois) { - var obj = ois.readObject(); - assertThat(obj).isInstanceOf(Exc.class); - - var exc = Exc.class.cast(obj); - var cause = exc.getCause(); - assertThat(cause).isInstanceOf(IndexOutOfBoundsException.class); - assertThat(cause.getMessage()).isEqualTo("Index out of range: 4"); - - var robj = exc.getReason(); - assertThat(robj).isInstanceOf(SerializableReason.class); - var reason = SerializableReason.class.cast(robj); - assertThat(reason.name()).isEqualTo("data"); - assertThat(reason.index()).isEqualTo(4); - assertThat(reason.min()).isEqualTo(0); - assertThat(reason.max()).isEqualTo(3); - } - } + @Test + void reason_is_serializable_and_has_cause() throws Exception { + var bos = new ByteArrayOutputStream(); + var oos = new ObjectOutputStream(bos); + try (oos) { + var cause = new IndexOutOfBoundsException(4); + var exc = new Exc(new SerializableReason("data", 4, 0, 3), cause); + oos.writeObject(exc); + } + + var bytes = bos.toByteArray(); + var ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); + try (ois) { + var obj = ois.readObject(); + assertThat(obj).isInstanceOf(Exc.class); + + var exc = Exc.class.cast(obj); + var cause = exc.getCause(); + assertThat(cause).isInstanceOf(IndexOutOfBoundsException.class); + assertThat(cause.getMessage()).isEqualTo("Index out of range: 4"); + + var robj = exc.getReason(); + assertThat(robj).isInstanceOf(SerializableReason.class); + var reason = SerializableReason.class.cast(robj); + assertThat(reason.name()).isEqualTo("data"); + assertThat(reason.index()).isEqualTo(4); + assertThat(reason.min()).isEqualTo(0); + assertThat(reason.max()).isEqualTo(3); + } + } - @Test - void reason_is_not_serializable() throws Exception { - var bos = new ByteArrayOutputStream(); - var oos = new ObjectOutputStream(bos); - try (oos) { - var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); - oos.writeObject(exc); - fail(); - } catch (NotSerializableException e) { - assertThat(e.getMessage()).isEqualTo(IndexOutOfRange.class.getName()); - } - } + @Test + void reason_is_not_serializable() throws Exception { + var bos = new ByteArrayOutputStream(); + var oos = new ObjectOutputStream(bos); + try (oos) { + var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); + oos.writeObject(exc); + fail(); + } catch (NotSerializableException e) { + assertThat(e.getMessage()).isEqualTo(IndexOutOfRange.class.getName()); + } } + } }