--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.api;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.MoreObjects;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The contents of a {@code error-message} element as defined in
+ * <a href="https://www.rfc-editor.org/rfc/rfc8040#page-83">RFC8040 errors grouping</a>. This object can optionally
+ * transport the <a href="https://www.w3.org/TR/xml/#sec-lang-tag">Language Identification</a> as conveyed via,
+ * for example, <a href="https://www.rfc-editor.org/rfc/rfc6241#page-17">RFC6241 error-message element</a>.
+ *
+ * @param elementBody the string to be displayed
+ * @param xmlLang optional Language Identification string
+ */
+@NonNullByDefault
+// TODO: consider sharing this class with netconf-api's NetconfDocumentedException
+public record ErrorMessage(String elementBody, @Nullable String xmlLang) {
+ public ErrorMessage {
+ requireNonNull(elementBody);
+ }
+
+ public ErrorMessage(final String elementBody) {
+ this(elementBody, null);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this).omitNullValues()
+ .add("elementBody", elementBody)
+ .add("xmlLang", xmlLang)
+ .toString();
+ }
+}
\ No newline at end of file
* @author Devin Avery
* @author Thomas Pantelis
*/
+@Deprecated
public class RestconfDocumentedException extends RuntimeException {
@java.io.Serial
private static final long serialVersionUID = 3L;
*
* @author Devin Avery
*/
+@Deprecated
public class RestconfError implements Serializable {
@java.io.Serial
private static final long serialVersionUID = 1L;
* Returns the {@link YangInstanceIdentifier} of the instance being referenced.
*
* @return the {@link YangInstanceIdentifier} of the instance being referenced,
- * {@link YangInstanceIdentifier#empty()} denotes the datastora
+ * {@link YangInstanceIdentifier#empty()} denotes the data root
*/
YangInstanceIdentifier instance();
+
+ /**
+ * Returns this reference as a {@link ServerErrorPath}.
+ *
+ * @return this reference as a {@link ServerErrorPath}
+ */
+ default ServerErrorPath toErrorPath() {
+ return new ServerErrorPath(databind(), instance());
+ }
}
/**
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.server.api;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.MoreObjects;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.restconf.api.ErrorMessage;
+import org.opendaylight.yangtools.yang.common.ErrorTag;
+import org.opendaylight.yangtools.yang.common.ErrorType;
+
+/**
+ * Encapsulates a single {@code error} within the
+ * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-3.9">"errors" YANG Data Template</a> as bound to a particular
+ * {@link DatabindContext}.
+ *
+ * @param type value of {@code error-type} leaf
+ * @param tag value of {@code error-tag} leaf
+ * @param message value of {@code error-message} leaf, potentially with metadata
+ * @param appTag value of {@code error-api-tag} leaf
+ * @param path optional {@code error-path} leaf
+ * @param info optional content of {@code error-info} anydata
+ */
+@NonNullByDefault
+public record ServerError(
+ ErrorType type,
+ ErrorTag tag,
+ @Nullable ErrorMessage message,
+ @Nullable String appTag,
+ @Nullable ServerErrorPath path,
+ @Nullable ServerErrorInfo info) {
+ public ServerError {
+ requireNonNull(type);
+ requireNonNull(tag);
+ }
+
+ public ServerError(final ErrorType type, final ErrorTag tag, final String message) {
+ this(type, tag, new ErrorMessage(message), null, null, null);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this).omitNullValues()
+ .add("type", type)
+ .add("tag", tag)
+ .add("appTag", appTag)
+ .add("message", message)
+ .add("path", path)
+ .add("info", info)
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.server.api;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.Beta;
+
+/**
+ * The content of {@code error-info} {@code anydata}.
+ */
+// FIXME: String here is legacy coming from RestconfError. This really should be a FormattableBody or similar, i.e.
+// structured content which itself is formattable -- unlike FormattableBody, though, it needs to be defined as
+// being formatted to a output. This format should include writing.
+// FIXME: given that the normalized-node-based FormattableBody lives in server.spi, this should probably be an interface
+// implemented in at server.spi level.
+@Beta
+public record ServerErrorInfo(String value) {
+ public ServerErrorInfo {
+ requireNonNull(value);
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.server.api;
+
+import static java.util.Objects.requireNonNull;
+
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+
+/**
+ * An {@code error-path} element in a {@link ServerError}.
+ *
+ * @param databind the {@link DatabindContext} to which this path is bound
+ * @param path the {@link YangInstanceIdentifier}, {@link YangInstanceIdentifier#empty()} denotes the data root
+ */
+public record ServerErrorPath(DatabindContext databind, YangInstanceIdentifier path) {
+ public ServerErrorPath {
+ requireNonNull(databind);
+ requireNonNull(path);
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.server.api;
+
+import static java.util.Objects.requireNonNull;
+
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamException;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.restconf.api.ErrorMessage;
+import org.opendaylight.yangtools.yang.common.ErrorTag;
+import org.opendaylight.yangtools.yang.common.ErrorType;
+
+/**
+ * A server-side processing exception, reporting a single {@link ServerError}. This exception is not serializable on
+ * purpose.
+ */
+@NonNullByDefault
+public final class ServerException extends Exception {
+ @java.io.Serial
+ private static final long serialVersionUID = 0L;
+
+ private final ServerError error;
+
+ private ServerException(final String message, final ServerError error, final @Nullable Throwable cause) {
+ super(message, cause);
+ this.error = requireNonNull(error);
+ }
+
+ public ServerException(final String message) {
+ this(ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, requireNonNull(message));
+ }
+
+ public ServerException(final String format, final Object @Nullable ... args) {
+ this(ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, format, args);
+ }
+
+ public ServerException(final Throwable cause) {
+ this(ErrorType.APPLICATION, errorTagOf(cause), cause);
+ }
+
+ public ServerException(final String message, final @Nullable Throwable cause) {
+ this(ErrorType.APPLICATION, errorTagOf(cause), requireNonNull(message), cause);
+ }
+
+ public ServerException(final ErrorType type, final ErrorTag tag, final String message) {
+ this(type, tag, message, (Throwable) null);
+ }
+
+ public ServerException(final ErrorType type, final ErrorTag tag, final Throwable cause) {
+ this(cause.toString(), new ServerError(type, tag, new ErrorMessage(cause.getMessage()), null, null, null),
+ cause);
+ }
+
+ public ServerException(final ErrorType type, final ErrorTag tag, final String message,
+ final @Nullable Throwable cause) {
+ this(requireNonNull(message), new ServerError(type, tag, message), cause);
+ }
+
+ public ServerException(final ErrorType type, final ErrorTag tag, final String format,
+ final Object @Nullable ... args) {
+ this(type, tag, format.formatted(args));
+ }
+
+ /**
+ * Return the reported {@link ServerError}.
+ *
+ * @return the reported {@link ServerError}
+ */
+ public ServerError error() {
+ return error;
+ }
+
+ @java.io.Serial
+ private void readObjectNoData() throws ObjectStreamException {
+ throw new NotSerializableException();
+ }
+
+ @java.io.Serial
+ private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException {
+ throw new NotSerializableException();
+ }
+
+ @java.io.Serial
+ private void writeObject(final ObjectOutputStream stream) throws IOException {
+ throw new NotSerializableException();
+ }
+
+ private static ErrorTag errorTagOf(final @Nullable Throwable cause) {
+ if (cause instanceof UnsupportedOperationException) {
+ return ErrorTag.OPERATION_NOT_SUPPORTED;
+ } else if (cause instanceof IllegalArgumentException) {
+ return ErrorTag.INVALID_VALUE;
+ } else {
+ return ErrorTag.OPERATION_FAILED;
+ }
+ }
+}
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
/**
- * Interface to a RESTCONF server instance. The primary entry point is {@link RestconfServer}.
+ * Interface to a RESTCONF server instance. The primary entry point is {@link RestconfServer}, which is typically given
+ * a {@link ServerRequest} coupled with an {@link org.opendaylight.restconf.api.ApiPath} and perhaps a
+ * {@link RequestBody}.
*/
package org.opendaylight.restconf.server.api;
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.server.api;
+
+import static org.junit.Assert.assertSame;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+import org.opendaylight.yangtools.yang.common.ErrorTag;
+import org.opendaylight.yangtools.yang.common.ErrorType;
+
+class ServerExceptionTest {
+ @Test
+ void stringConstructor() {
+ final var ex = new ServerException("some message");
+ assertEquals("some message", ex.getMessage());
+ assertNull(ex.getCause());
+ assertEquals(new ServerError(ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, "some message"), ex.error());
+ }
+
+ @Test
+ void causeConstructor() {
+ final var cause = new Throwable("cause message");
+ final var ex = new ServerException(cause);
+ assertEquals("java.lang.Throwable: cause message", ex.getMessage());
+ assertSame(cause, ex.getCause());
+ assertEquals(new ServerError(ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, "cause message"), ex.error());
+ }
+
+ @Test
+ void causeConstructorIAE() {
+ final var cause = new IllegalArgumentException("cause message");
+ final var ex = new ServerException(cause);
+ assertEquals("java.lang.IllegalArgumentException: cause message", ex.getMessage());
+ assertSame(cause, ex.getCause());
+ assertEquals(new ServerError(ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, "cause message"), ex.error());
+ }
+
+ @Test
+ void causeConstructorUOE() {
+ final var cause = new UnsupportedOperationException("cause message");
+ final var ex = new ServerException(cause);
+ assertEquals("java.lang.UnsupportedOperationException: cause message", ex.getMessage());
+ assertSame(cause, ex.getCause());
+ assertEquals(new ServerError(ErrorType.APPLICATION, ErrorTag.OPERATION_NOT_SUPPORTED, "cause message"),
+ ex.error());
+ }
+
+ @Test
+ void messageCauseConstructor() {
+ final var cause = new Throwable("cause message");
+ final var ex = new ServerException("some message", cause);
+ assertEquals("some message", ex.getMessage());
+ assertSame(cause, ex.getCause());
+ assertEquals(new ServerError(ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, "some message"), ex.error());
+ }
+
+ @Test
+ void messageCauseConstructorIAE() {
+ final var cause = new IllegalArgumentException("cause message");
+ final var ex = new ServerException("some message", cause);
+ assertEquals("some message", ex.getMessage());
+ assertSame(cause, ex.getCause());
+ assertEquals(new ServerError(ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, "some message"), ex.error());
+ }
+
+ @Test
+ void messageCauseConstructorUOE() {
+ final var cause = new UnsupportedOperationException("cause message");
+ final var ex = new ServerException("some message", cause);
+ assertEquals("some message", ex.getMessage());
+ assertSame(cause, ex.getCause());
+ assertEquals(new ServerError(ErrorType.APPLICATION, ErrorTag.OPERATION_NOT_SUPPORTED, "some message"),
+ ex.error());
+ }
+
+ @Test
+ void formatConstructor() {
+ final var ex = new ServerException("huh %s: %s", 1, "hah");
+ assertEquals("huh 1: hah", ex.getMessage());
+ assertNull(ex.getCause());
+ assertEquals(new ServerError(ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, "huh 1: hah"), ex.error());
+ }
+}