Skip to content

Commit 095d16b

Browse files
author
Pritham Marupaka
committed
wip: Add checked service exception
1 parent 8c3505f commit 095d16b

File tree

4 files changed

+345
-78
lines changed

4 files changed

+345
-78
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* (c) Copyright 2024 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.palantir.conjure.java.api.errors;
18+
19+
import com.palantir.logsafe.Arg;
20+
import com.palantir.logsafe.SafeLoggable;
21+
import java.util.List;
22+
import javax.annotation.Nullable;
23+
24+
/**
25+
* An exception raised by a service to indicate an expected error state.
26+
*/
27+
public abstract class CheckedServiceException extends Exception implements SafeLoggable {
28+
private static final String EXCEPTION_NAME = "CheckedServiceException";
29+
private final ErrorType errorType;
30+
private final List<Arg<?>> args; // This is an unmodifiable list.
31+
private final String errorInstanceId;
32+
private final String unsafeMessage;
33+
private final String noArgsMessage;
34+
35+
/**
36+
* Creates a new exception for the given error.
37+
*/
38+
public CheckedServiceException(ErrorType errorType, Arg<?>... parameters) {
39+
this(errorType, null, parameters);
40+
}
41+
42+
/** As above, but additionally records the cause of this exception. */
43+
public CheckedServiceException(ErrorType errorType, @Nullable Throwable cause, Arg<?>... args) {
44+
super(cause);
45+
this.errorInstanceId = ServiceExceptionUtils.generateErrorInstanceId(cause);
46+
this.errorType = errorType;
47+
this.args = ServiceExceptionUtils.arrayToUnmodifiableList(args);
48+
this.unsafeMessage = ServiceExceptionUtils.renderUnsafeMessage(EXCEPTION_NAME, errorType, args);
49+
this.noArgsMessage = ServiceExceptionUtils.renderNoArgsMessage(EXCEPTION_NAME, errorType);
50+
}
51+
52+
/** The {@link ErrorType} that gave rise to this exception. */
53+
public ErrorType getErrorType() {
54+
return errorType;
55+
}
56+
57+
/** A unique identifier for (this instance of) this error. */
58+
public String getErrorInstanceId() {
59+
return errorInstanceId;
60+
}
61+
62+
/** A string that includes the exception name, error type, and all arguments irrespective of log-safety. */
63+
@Override
64+
public String getMessage() {
65+
// Including all args here since any logger not configured with safe-logging will log this message.
66+
return unsafeMessage;
67+
}
68+
69+
/** A string that includes the exception name and error type, without any arguments. */
70+
@Override
71+
public String getLogMessage() {
72+
return noArgsMessage;
73+
}
74+
75+
/** The list of arguments. */
76+
@Override
77+
public List<Arg<?>> getArgs() {
78+
return args;
79+
}
80+
}

errors/src/main/java/com/palantir/conjure/java/api/errors/ServiceException.java

Lines changed: 5 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,12 @@
1818

1919
import com.palantir.logsafe.Arg;
2020
import com.palantir.logsafe.SafeLoggable;
21-
import com.palantir.tritium.ids.UniqueIds;
22-
import java.util.ArrayList;
23-
import java.util.Collections;
24-
import java.util.IdentityHashMap;
2521
import java.util.List;
26-
import java.util.Set;
2722
import javax.annotation.Nullable;
2823

2924
/** A {@link ServiceException} thrown in server-side code to indicate server-side {@link ErrorType error states}. */
3025
public final class ServiceException extends RuntimeException implements SafeLoggable {
31-
26+
private static final String EXCEPTION_NAME = "ServiceException";
3227
private final ErrorType errorType;
3328
private final List<Arg<?>> args; // unmodifiable
3429

@@ -49,12 +44,12 @@ public ServiceException(ErrorType errorType, @Nullable Throwable cause, Arg<?>..
4944
// TODO(rfink): Memoize formatting?
5045
super(cause);
5146

52-
this.errorInstanceId = generateErrorInstanceId(cause);
47+
this.errorInstanceId = ServiceExceptionUtils.generateErrorInstanceId(cause);
5348
this.errorType = errorType;
5449
// Note that instantiators cannot mutate List<> args since it comes through copyToList in all code paths.
55-
this.args = copyToUnmodifiableList(args);
56-
this.unsafeMessage = renderUnsafeMessage(errorType, args);
57-
this.noArgsMessage = renderNoArgsMessage(errorType);
50+
this.args = ServiceExceptionUtils.arrayToUnmodifiableList(args);
51+
this.unsafeMessage = ServiceExceptionUtils.renderUnsafeMessage(EXCEPTION_NAME, errorType, args);
52+
this.noArgsMessage = ServiceExceptionUtils.renderNoArgsMessage(EXCEPTION_NAME, errorType);
5853
}
5954

6055
/** The {@link ErrorType} that gave rise to this exception. */
@@ -93,72 +88,4 @@ public List<Arg<?>> getArgs() {
9388
public List<Arg<?>> getParameters() {
9489
return getArgs();
9590
}
96-
97-
private static <T> List<T> copyToUnmodifiableList(T[] elements) {
98-
if (elements == null || elements.length == 0) {
99-
return Collections.emptyList();
100-
}
101-
List<T> list = new ArrayList<>(elements.length);
102-
for (T item : elements) {
103-
if (item != null) {
104-
list.add(item);
105-
}
106-
}
107-
return Collections.unmodifiableList(list);
108-
}
109-
110-
private static String renderUnsafeMessage(ErrorType errorType, Arg<?>... args) {
111-
String message = renderNoArgsMessage(errorType);
112-
113-
if (args == null || args.length == 0) {
114-
return message;
115-
}
116-
117-
StringBuilder builder = new StringBuilder();
118-
boolean first = true;
119-
builder.append(message).append(": {");
120-
for (Arg<?> arg : args) {
121-
if (arg != null) {
122-
if (first) {
123-
first = false;
124-
} else {
125-
builder.append(", ");
126-
}
127-
builder.append(arg.getName()).append("=").append(arg.getValue());
128-
}
129-
}
130-
builder.append("}");
131-
132-
return builder.toString();
133-
}
134-
135-
private static String renderNoArgsMessage(ErrorType errorType) {
136-
return "ServiceException: " + errorType.code() + " (" + errorType.name() + ")";
137-
}
138-
139-
/**
140-
* Finds the errorInstanceId of the most recent cause if present, otherwise generates a new random identifier. Note
141-
* that this only searches {@link Throwable#getCause() causal exceptions}, not {@link Throwable#getSuppressed()
142-
* suppressed causes}.
143-
*/
144-
private static String generateErrorInstanceId(@Nullable Throwable cause) {
145-
return generateErrorInstanceId(cause, Collections.newSetFromMap(new IdentityHashMap<>()));
146-
}
147-
148-
private static String generateErrorInstanceId(
149-
@Nullable Throwable cause,
150-
// Guard against cause cycles, see Throwable.printStackTrace(PrintStreamOrWriter)
151-
Set<Throwable> dejaVu) {
152-
if (cause == null || !dejaVu.add(cause)) {
153-
// we don't need cryptographically secure random UUIDs
154-
return UniqueIds.pseudoRandomUuidV4().toString();
155-
}
156-
if (cause instanceof ServiceException) {
157-
return ((ServiceException) cause).getErrorInstanceId();
158-
}
159-
if (cause instanceof RemoteException) {
160-
return ((RemoteException) cause).getError().errorInstanceId();
161-
}
162-
return generateErrorInstanceId(cause.getCause(), dejaVu);
163-
}
16491
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* (c) Copyright 2024 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.palantir.conjure.java.api.errors;
18+
19+
import com.palantir.logsafe.Arg;
20+
import com.palantir.tritium.ids.UniqueIds;
21+
import java.util.ArrayList;
22+
import java.util.Collections;
23+
import java.util.IdentityHashMap;
24+
import java.util.List;
25+
import java.util.Set;
26+
import javax.annotation.Nullable;
27+
28+
/**
29+
* This class is a collection of methods useful for creating {@link ServiceException}s and {@link CheckedServiceException}s.
30+
*/
31+
final class ServiceExceptionUtils {
32+
private ServiceExceptionUtils() {}
33+
34+
/**
35+
* Creates an unmodifiable list from the given array. Null entries are filtered out as unmodifiable lists cannot
36+
* have null elements.
37+
*
38+
* @param elements the array to convert to an unmodifiable list
39+
* @return an unmodifiable list containing the non-null elements of the array
40+
* @see <a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Collection.html#unmodview">unmodifiable view</a>
41+
*/
42+
static <T> List<T> arrayToUnmodifiableList(T[] elements) {
43+
if (elements == null || elements.length == 0) {
44+
return Collections.emptyList();
45+
}
46+
List<T> list = new ArrayList<>(elements.length);
47+
for (T item : elements) {
48+
if (item != null) {
49+
list.add(item);
50+
}
51+
}
52+
return Collections.unmodifiableList(list);
53+
}
54+
55+
/**
56+
* Create a message string that includes the exception name, error type, and all arguments irrespective of log
57+
* safety.
58+
*
59+
* @param exceptionName the name of the exception for which the message is being rendered
60+
* @param errorType the error type the exception represents
61+
* @param args the arguments to be included in the message
62+
* @return a message string that includes the exception name, error type, and arguments
63+
*/
64+
static String renderUnsafeMessage(String exceptionName, ErrorType errorType, Arg<?>... args) {
65+
String message = renderNoArgsMessage(exceptionName, errorType);
66+
67+
if (args == null || args.length == 0) {
68+
return message;
69+
}
70+
71+
StringBuilder builder = new StringBuilder();
72+
builder.append(message).append(": {");
73+
boolean first = true;
74+
for (Arg<?> arg : args) {
75+
if (arg == null) {
76+
continue;
77+
}
78+
if (first) {
79+
first = false;
80+
} else {
81+
builder.append(", ");
82+
}
83+
builder.append(arg.getName()).append("=").append(arg.getValue());
84+
}
85+
builder.append("}");
86+
87+
return builder.toString();
88+
}
89+
90+
/**
91+
* Create a message string that includes the exception name and error type, but no arguments.
92+
*
93+
* @param exceptionName the name of the exception for which the message is being rendered
94+
* @param errorType the error type the exception represents
95+
* @return a message string
96+
*/
97+
static String renderNoArgsMessage(String exceptionName, ErrorType errorType) {
98+
return exceptionName + ": " + errorType.code() + " (" + errorType.name() + ")";
99+
}
100+
101+
/**
102+
* Finds the errorInstanceId of the most recent cause if present, otherwise generates a new random identifier. Note
103+
* that this only searches {@link Throwable#getCause() causal exceptions}, not {@link Throwable#getSuppressed()
104+
* suppressed causes}.
105+
*/
106+
// VisibleForTesting
107+
static String generateErrorInstanceId(@Nullable Throwable cause) {
108+
return generateErrorInstanceId(cause, Collections.newSetFromMap(new IdentityHashMap<>()));
109+
}
110+
111+
private static String generateErrorInstanceId(
112+
@Nullable Throwable cause,
113+
// Guard against cause cycles, see Throwable.printStackTrace(PrintStreamOrWriter)
114+
Set<Throwable> dejaVu) {
115+
if (cause == null || !dejaVu.add(cause)) {
116+
// we don't need cryptographically secure random UUIDs
117+
return UniqueIds.pseudoRandomUuidV4().toString();
118+
}
119+
if (cause instanceof ServiceException) {
120+
return ((ServiceException) cause).getErrorInstanceId();
121+
}
122+
if (cause instanceof CheckedServiceException) {
123+
return ((CheckedServiceException) cause).getErrorInstanceId();
124+
}
125+
if (cause instanceof RemoteException) {
126+
return ((RemoteException) cause).getError().errorInstanceId();
127+
}
128+
return generateErrorInstanceId(cause.getCause(), dejaVu);
129+
}
130+
}

0 commit comments

Comments
 (0)