diff --git a/coffee-grpc/coffee-grpc-api/src/main/java/hu/icellmobilsoft/coffee/grpc/api/metadata/IGrpcHeader.java b/coffee-grpc/coffee-grpc-api/src/main/java/hu/icellmobilsoft/coffee/grpc/api/metadata/IGrpcHeader.java new file mode 100644 index 000000000..5b0f56c16 --- /dev/null +++ b/coffee-grpc/coffee-grpc-api/src/main/java/hu/icellmobilsoft/coffee/grpc/api/metadata/IGrpcHeader.java @@ -0,0 +1,43 @@ +/*- + * #%L + * Coffee + * %% + * Copyright (C) 2020 - 2024 i-Cell Mobilsoft Zrt. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package hu.icellmobilsoft.coffee.grpc.api.metadata; + +import io.grpc.Metadata; +import io.grpc.Metadata.Key; + +/** + * Grpc Header metadata constants + * + * @author Imre Scheffer + * @since 2.7.0 + */ +public interface IGrpcHeader { + + /** + * Language Metadata header key + */ + Metadata.Key HEADER_LANGUAGE = Key.of("X-LANGUAGE", Metadata.ASCII_STRING_MARSHALLER); + + /** + * Logging, global transaction session id header key + */ + Metadata.Key HEADER_SID = Metadata.Key.of("X-SID", Metadata.ASCII_STRING_MARSHALLER); + +} diff --git a/coffee-grpc/coffee-grpc-base/src/main/java/hu/icellmobilsoft/coffee/grpc/base/exception/ExceptionHandler.java b/coffee-grpc/coffee-grpc-base/src/main/java/hu/icellmobilsoft/coffee/grpc/base/exception/ExceptionHandler.java index cd0a85782..e316cef51 100644 --- a/coffee-grpc/coffee-grpc-base/src/main/java/hu/icellmobilsoft/coffee/grpc/base/exception/ExceptionHandler.java +++ b/coffee-grpc/coffee-grpc-base/src/main/java/hu/icellmobilsoft/coffee/grpc/base/exception/ExceptionHandler.java @@ -45,6 +45,7 @@ import com.google.rpc.Status; import hu.icellmobilsoft.coffee.se.logging.Logger; +import io.grpc.Metadata; /** * The ExceptionHandler class serves to handle exceptions in gRPC services using ExceptionMappers, similar to how they are used in JAX-RS @@ -96,22 +97,24 @@ public static ExceptionHandler getInstance() { * * @param * Generic type of the exception + * @param requestHeaders + * Grpc request metadata * @param t * the exception to be handled * @return the corresponding {@link StatusResponse} */ - public StatusResponse handle(E t) { + public StatusResponse handle(Metadata requestHeaders, E t) { if (t instanceof GrpcRuntimeExceptionWrapper && ((GrpcRuntimeExceptionWrapper) t).getWrapped() != null) { - return handleStatus(((GrpcRuntimeExceptionWrapper) t).getWrapped()); + return handleStatus(requestHeaders, ((GrpcRuntimeExceptionWrapper) t).getWrapped()); } - return handleStatus(t); + return handleStatus(requestHeaders, t); } - private StatusResponse handleStatus(E t) { + private StatusResponse handleStatus(Metadata requestHeaders, E t) { try { List> mapperBeans = getExceptionMapperBeans(t.getClass()); for (Bean exceptionMapperBean : mapperBeans) { - Status status = handleByBean(t, exceptionMapperBean); + Status status = handleByBean(requestHeaders, t, exceptionMapperBean); // ha nem null visszaadjuk, ha null jön priority szerint a következő, esetleg az exception super class-ára írt if (status != null) { return StatusResponse.of(status, t); @@ -121,17 +124,18 @@ private StatusResponse handleStatus(E t) { LOG.error("Error occurred in ExceptionHandler - " + e.getMessage(), e); return buildInternalErrorStatus(t, e.getMessage()); } - return buildInternalErrorStatus(t, "Could not find ExceptionMapper"); + String reason = MessageFormat.format("Could not find " + ExceptionMapper.class.getName() + " CDI implementation for [{0}]", t.getClass()); + return buildInternalErrorStatus(t, reason); } @SuppressWarnings({ "rawtypes", "unchecked" }) - private Status handleByBean(E t, Bean exceptionMapperBean) { + private Status handleByBean(Metadata requestHeaders, E t, Bean exceptionMapperBean) { Instance instance = (Instance) CDI.current().select(exceptionMapperBean.getBeanClass()); if (instance.isResolvable()) { ExceptionMapper exceptionMapper = null; try { exceptionMapper = instance.get(); - Status status = exceptionMapper.toStatus(t); + Status status = exceptionMapper.toStatus(requestHeaders, t); if (status != null) { return status; } @@ -179,11 +183,13 @@ private StatusResponse buildInternalErrorStatus(Throwable throwableNotHandled, S // ha exception mapper elszáll, akkor internal server error. Status.Builder statusBuilder = Status.newBuilder(); + // ha nincs ExceptionHandler: + // 1. INTERNAL status kod statusBuilder.setCode(Code.INTERNAL.getNumber()); - statusBuilder.setMessage("Could not handle error - " + throwableNotHandled.getMessage()); - statusBuilder.addDetails(Any.pack(ErrorInfo.newBuilder() // - .setReason(reason) // - .setDomain(throwableNotHandled.getClass().toString()).build())); + // 2. valaszolunk eredeti hibaval + statusBuilder.setMessage(throwableNotHandled.getMessage()); + // 3. ErrorInfo-ba pakoljuk a reszleteket + statusBuilder.addDetails(Any.pack(ErrorInfo.newBuilder().setReason(reason).setDomain(throwableNotHandled.getClass().getName()).build())); return StatusResponse.of(statusBuilder.build(), throwableNotHandled); } } diff --git a/coffee-grpc/coffee-grpc-base/src/main/java/hu/icellmobilsoft/coffee/grpc/base/exception/ExceptionMapper.java b/coffee-grpc/coffee-grpc-base/src/main/java/hu/icellmobilsoft/coffee/grpc/base/exception/ExceptionMapper.java index b81a97c93..7af74afbc 100644 --- a/coffee-grpc/coffee-grpc-base/src/main/java/hu/icellmobilsoft/coffee/grpc/base/exception/ExceptionMapper.java +++ b/coffee-grpc/coffee-grpc-base/src/main/java/hu/icellmobilsoft/coffee/grpc/base/exception/ExceptionMapper.java @@ -21,6 +21,8 @@ import com.google.rpc.Status; +import io.grpc.Metadata; + /** * ExceptionMapper is an interface for mapping an exception of type E to a gRPC {@link com.google.rpc.Status}. *

@@ -29,6 +31,7 @@ * @param * The type of exception to be mapped to a gRPC Status * @author mark.petrenyi + * @author Imre Scheffer * @since 2.1.0 */ public interface ExceptionMapper { @@ -36,10 +39,12 @@ public interface ExceptionMapper { /** * Maps an exception of type E to a gRPC Status. * + * @param requestHeaders + * Incoming Grpc request headers * @param e * The exception to be mapped * @return The gRPC Status resulting from the mapping */ - Status toStatus(E e); + Status toStatus(Metadata requestHeaders, E e); } diff --git a/coffee-grpc/coffee-grpc-base/src/main/java/hu/icellmobilsoft/coffee/grpc/base/exception/StatusResponse.java b/coffee-grpc/coffee-grpc-base/src/main/java/hu/icellmobilsoft/coffee/grpc/base/exception/StatusResponse.java index f1aa979c4..dab889f5a 100644 --- a/coffee-grpc/coffee-grpc-base/src/main/java/hu/icellmobilsoft/coffee/grpc/base/exception/StatusResponse.java +++ b/coffee-grpc/coffee-grpc-base/src/main/java/hu/icellmobilsoft/coffee/grpc/base/exception/StatusResponse.java @@ -54,12 +54,12 @@ public class StatusResponse { private final Metadata metadata; private StatusResponse(com.google.rpc.Status statusProto) { - status = Status.fromCodeValue(statusProto.getCode()); + status = Status.fromCodeValue(statusProto.getCode()).withDescription(statusProto.getMessage()); metadata = toMetadata(statusProto); } private StatusResponse(com.google.rpc.Status statusProto, Throwable cause) { - status = Status.fromCodeValue(statusProto.getCode()).withCause(cause); + status = Status.fromCodeValue(statusProto.getCode()).withDescription(statusProto.getMessage()).withCause(cause); metadata = toMetadata(statusProto); } diff --git a/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/interceptor/ErrorHandlerInterceptor.java b/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/interceptor/ErrorHandlerInterceptor.java index 7ab26cbfa..2fb9a717d 100644 --- a/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/interceptor/ErrorHandlerInterceptor.java +++ b/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/interceptor/ErrorHandlerInterceptor.java @@ -82,7 +82,7 @@ public void onMessage(ReqT message) { try { super.onMessage(message); } catch (Throwable e) { - StatusResponse status = ExceptionHandler.getInstance().handle(e); + StatusResponse status = ExceptionHandler.getInstance().handle(headers, e); serverCall.close(status.getStatus(), status.getMetadata()); } } @@ -92,7 +92,7 @@ public void onHalfClose() { try { super.onHalfClose(); } catch (Throwable e) { - StatusResponse status = ExceptionHandler.getInstance().handle(e); + StatusResponse status = ExceptionHandler.getInstance().handle(headers, e); serverCall.close(status.getStatus(), status.getMetadata()); } } diff --git a/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/interceptor/ServerRequestInterceptor.java b/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/interceptor/ServerRequestInterceptor.java index e84648b93..dc983055e 100644 --- a/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/interceptor/ServerRequestInterceptor.java +++ b/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/interceptor/ServerRequestInterceptor.java @@ -31,7 +31,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.reflect.MethodUtils; -import hu.icellmobilsoft.coffee.dto.common.LogConstants; +import hu.icellmobilsoft.coffee.grpc.api.metadata.IGrpcHeader; import hu.icellmobilsoft.coffee.grpc.server.log.GrpcLogging; import hu.icellmobilsoft.coffee.rest.log.annotation.LogSpecifier; import hu.icellmobilsoft.coffee.rest.log.annotation.LogSpecifiers; @@ -72,7 +72,7 @@ public ServerRequestInterceptor() { @Override public Listener interceptCall(ServerCall serverCall, Metadata headers, ServerCallHandler next) { - String extSessionIdHeader = headers.get(Metadata.Key.of(LogConstants.LOG_SESSION_ID, Metadata.ASCII_STRING_MARSHALLER)); + String extSessionIdHeader = headers.get(IGrpcHeader.HEADER_SID); String extSessionId = StringUtils.isNotBlank(extSessionIdHeader) ? extSessionIdHeader : RandomUtil.generateId(); Context context = Context.current().withValue(GrpcLogging.CONTEXT_KEY_SESSIONID, extSessionId); diff --git a/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/mapper/DefaultGrpcExceptionTranslator.java b/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/mapper/DefaultGrpcExceptionTranslator.java new file mode 100644 index 000000000..498c77695 --- /dev/null +++ b/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/mapper/DefaultGrpcExceptionTranslator.java @@ -0,0 +1,68 @@ +/*- + * #%L + * Coffee + * %% + * Copyright (C) 2020 - 2024 i-Cell Mobilsoft Zrt. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package hu.icellmobilsoft.coffee.grpc.server.mapper; + +import java.io.Serializable; +import java.util.Locale; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import com.google.rpc.LocalizedMessage; +import com.google.rpc.LocalizedMessage.Builder; + +/** + * Default implementation for translating exceptions to status. + * + * @author Imre Scheffer + * @since 2.7.0 + */ +@ApplicationScoped +public class DefaultGrpcExceptionTranslator implements IGrpcExceptionTranslator { + + /** + * Default Locale for Grpc message translation + */ + public static final Locale DEFAULT_LOCALE = Locale.ENGLISH; + + @Inject + private hu.icellmobilsoft.coffee.module.localization.LocalizedMessage localizedMessage; + + /** + * Default constructor, constructs a new object. + */ + public DefaultGrpcExceptionTranslator() { + super(); + } + + @Override + public Builder toLocalizedMessage(Locale locale, Enum faultType, Serializable... messageArguments) { + LocalizedMessage.Builder lmBuilder = LocalizedMessage.newBuilder(); + if (faultType != null) { + Locale returnLocale = locale == null ? DEFAULT_LOCALE : locale; + // localizedMessage.message(faultType, messageArguments) jelenleg nem megfelelo, + // mert REST headerbol olvassa a nyelvesito kulcsot es itt most nincs request scope sem + String translatedMessage = localizedMessage.messageByLanguage(returnLocale.getLanguage(), + "{" + faultType.getClass().getName() + "." + faultType.name() + "}", messageArguments); + lmBuilder.setLocale(returnLocale.toString()).setMessage(translatedMessage); + } + return lmBuilder; + } +} diff --git a/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/mapper/GrpcBaseExceptionMapper.java b/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/mapper/GrpcBaseExceptionMapper.java new file mode 100644 index 000000000..7b15dc158 --- /dev/null +++ b/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/mapper/GrpcBaseExceptionMapper.java @@ -0,0 +1,132 @@ +/*- + * #%L + * Coffee + * %% + * Copyright (C) 2020 - 2024 i-Cell Mobilsoft Zrt. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package hu.icellmobilsoft.coffee.grpc.server.mapper; + +import java.util.Locale; + +import jakarta.annotation.Priority; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import com.google.protobuf.Any; +import com.google.rpc.Code; +import com.google.rpc.DebugInfo; +import com.google.rpc.ErrorInfo; +import com.google.rpc.LocalizedMessage; +import com.google.rpc.Status; + +import hu.icellmobilsoft.coffee.dto.exception.AccessDeniedException; +import hu.icellmobilsoft.coffee.dto.exception.BONotFoundException; +import hu.icellmobilsoft.coffee.dto.exception.ServiceUnavailableException; +import hu.icellmobilsoft.coffee.grpc.base.exception.ExceptionMapper; +import hu.icellmobilsoft.coffee.grpc.server.metadata.GrpcHeaderHelper; +import hu.icellmobilsoft.coffee.rest.projectstage.ProjectStage; +import hu.icellmobilsoft.coffee.se.api.exception.BaseException; +import hu.icellmobilsoft.coffee.se.api.exception.BusinessException; +import hu.icellmobilsoft.coffee.se.api.exception.DtoConversionException; +import hu.icellmobilsoft.coffee.se.logging.Logger; +import io.grpc.Metadata; + +/** + * Implementation of {@link ExceptionMapper} that maps {@link BaseException} to gRPC {@link Status}. + * + * @author mark.petrenyi + * @author Imre Scheffer + * @since 2.7.0 + */ +@ApplicationScoped +@Priority(1) +public class GrpcBaseExceptionMapper implements ExceptionMapper { + + @Inject + private Logger log; + + @Inject + private IGrpcExceptionTranslator grpcExceptionTranslator; + + @Inject + private ProjectStage projectStage; + + /** + * Default constructor, constructs a new object. + */ + public GrpcBaseExceptionMapper() { + super(); + } + + @Override + public Status toStatus(Metadata requestHeaders, BaseException e) { + Status.Builder statusB = null; + Locale locale = getRequestLocale(requestHeaders); + if (e instanceof AccessDeniedException) { + statusB = createStatus(locale, Code.UNAUTHENTICATED, e); + } else if (e instanceof BONotFoundException) { + statusB = createStatus(locale, Code.NOT_FOUND, e); + } else if (e instanceof DtoConversionException || e instanceof hu.icellmobilsoft.coffee.dto.exception.DtoConversionException) { + statusB = createStatus(locale, Code.INVALID_ARGUMENT, e); + } else if (e instanceof ServiceUnavailableException) { + statusB = createStatus(locale, Code.UNAVAILABLE, e); + } else if (e instanceof BusinessException || e instanceof hu.icellmobilsoft.coffee.dto.exception.BusinessException) { + statusB = createStatus(locale, Code.FAILED_PRECONDITION, e); + } + + if (statusB == null) { + statusB = createStatus(locale, Code.INTERNAL, e); + } + return statusB.build(); + } + + /** + * Get Locale parameter from Grpc request metadata + * + * @param requestHeaders + * Grpc request metadata + * @return Locale from request + */ + protected Locale getRequestLocale(Metadata requestHeaders) { + return GrpcHeaderHelper.getRequestLocale(requestHeaders); + } + + /** + * Create Proto {@code com.google.rpc.Status} object from given parameters + * + * @param locale + * Locale to translate response error message + * @param grpcStatus + * Response Grpc status + * @param baseException + * Exception to packaged into Grpc response metadata + * @return Proto Status object + */ + protected Status.Builder createStatus(Locale locale, Code grpcStatus, BaseException baseException) { + Status.Builder result = Status.newBuilder(); + LocalizedMessage.Builder localizedMessageB = grpcExceptionTranslator.toLocalizedMessage(locale, baseException); + ErrorInfo.Builder errorInfoBuilder = GrpcGeneralExceptionMapper.toErrorInfo(baseException.getFaultTypeEnum()); + result.setCode(grpcStatus.getNumber()); + result.setMessage(baseException.getLocalizedMessage()); + result.addDetails(Any.pack(localizedMessageB.build())); + result.addDetails(Any.pack(errorInfoBuilder.build())); + if (!projectStage.isProductionStage()) { + DebugInfo.Builder debugInfoBuilder = GrpcGeneralExceptionMapper.toDebugInfo(baseException); + result.addDetails(Any.pack(debugInfoBuilder.build())); + } + return result; + } +} diff --git a/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/mapper/GrpcGeneralExceptionMapper.java b/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/mapper/GrpcGeneralExceptionMapper.java new file mode 100644 index 000000000..25ff2deb3 --- /dev/null +++ b/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/mapper/GrpcGeneralExceptionMapper.java @@ -0,0 +1,167 @@ +/*- + * #%L + * Coffee + * %% + * Copyright (C) 2020 - 2024 i-Cell Mobilsoft Zrt. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package hu.icellmobilsoft.coffee.grpc.server.mapper; + +import java.util.Arrays; +import java.util.Locale; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.NotAuthorizedException; + +import org.apache.commons.lang3.exception.ExceptionUtils; + +import com.google.protobuf.Any; +import com.google.rpc.Code; +import com.google.rpc.DebugInfo; +import com.google.rpc.ErrorInfo; +import com.google.rpc.LocalizedMessage; +import com.google.rpc.Status; + +import hu.icellmobilsoft.coffee.dto.exception.enums.CoffeeFaultType; +import hu.icellmobilsoft.coffee.grpc.base.exception.ExceptionMapper; +import hu.icellmobilsoft.coffee.grpc.server.metadata.GrpcHeaderHelper; +import hu.icellmobilsoft.coffee.rest.projectstage.ProjectStage; +import hu.icellmobilsoft.coffee.se.logging.Logger; +import io.grpc.Metadata; + +/** + * Implementation of {@link ExceptionMapper} that maps general Exceptions to gRPC {@link Status}. If the exception is not recognized, it maps it to + * {@link Code#INTERNAL}. + * + * It uses {@link IGrpcExceptionTranslator} to translate exceptions to GRPC statuses. + * + * @author mark.petrenyi + * @author Imre Scheffer + * @since 2.7.0 + */ +@ApplicationScoped +public class GrpcGeneralExceptionMapper implements ExceptionMapper { + + @Inject + private Logger log; + + @Inject + private IGrpcExceptionTranslator grpcExceptionTranslator; + + @Inject + private ProjectStage projectStage; + + /** + * Default constructor, constructs a new object. + */ + public GrpcGeneralExceptionMapper() { + super(); + } + + @Override + public Status toStatus(Metadata requestHeaders, Exception e) { + Status.Builder statusB = null; + Locale locale = getRequestLocale(requestHeaders); + if (e instanceof NotAuthorizedException) { + statusB = createStatus(locale, Code.UNAUTHENTICATED, CoffeeFaultType.NOT_AUTHORIZED, e); + } else if (e instanceof ForbiddenException) { + statusB = createStatus(locale, Code.PERMISSION_DENIED, CoffeeFaultType.FORBIDDEN, e); + } else if (e instanceof IllegalArgumentException || e.getCause() instanceof IllegalArgumentException) { + statusB = createStatus(locale, Code.INVALID_ARGUMENT, CoffeeFaultType.ILLEGAL_ARGUMENT_EXCEPTION, e); + } + + if (statusB != null) { + log.error("Known error: ", e); + } else { + log.error("Unknown error: ", e); + statusB = createStatus(locale, Code.INTERNAL, CoffeeFaultType.GENERIC_EXCEPTION, e); + } + return statusB.build(); + } + + /** + * Get Locale parameter from Grpc request metadata + * + * @param requestHeaders + * Grpc request metadata + * @return Locale from request + */ + protected Locale getRequestLocale(Metadata requestHeaders) { + return GrpcHeaderHelper.getRequestLocale(requestHeaders); + } + + /** + * Create Proto Error info object from given parameter + * + * @param faultEnum + * Application Error code packaged into result + * @return Proto Error info object + */ + public static ErrorInfo.Builder toErrorInfo(Enum faultEnum) { + ErrorInfo.Builder result = ErrorInfo.newBuilder(); + if (faultEnum != null) { + result.setReason(faultEnum.name()); + result.setDomain(faultEnum.getClass().getName()); + } + return result; + } + + /** + * Create Proto Debug info object from given parameters + * + * @param throwable + * Throwable packaged into result + * @return Proto Debug info object + */ + public static DebugInfo.Builder toDebugInfo(Throwable throwable) { + DebugInfo.Builder result = DebugInfo.newBuilder(); + if (throwable != null) { + String[] frames = ExceptionUtils.getStackFrames(throwable); + result.addAllStackEntries(Arrays.asList(frames)); + result.setDetail(ExceptionUtils.getMessage(throwable)); + } + return result; + } + + /** + * Create Proto {@code com.google.rpc.Status} object from given parameters + * + * @param locale + * Locale to translate response error message + * @param grpcStatus + * Response Grpc status + * @param faultType + * Error code + * @param exception + * Exception to packaged into Grpc response metadata + * @return Proto Status object + */ + protected Status.Builder createStatus(Locale locale, Code grpcStatus, Enum faultType, Exception exception) { + Status.Builder result = Status.newBuilder(); + LocalizedMessage.Builder localizedMessageB = grpcExceptionTranslator.toLocalizedMessage(locale, faultType); + ErrorInfo.Builder errorInfoBuilder = toErrorInfo(faultType); + result.setCode(grpcStatus.getNumber()); + result.setMessage(exception.getLocalizedMessage()); + result.addDetails(Any.pack(localizedMessageB.build())); + result.addDetails(Any.pack(errorInfoBuilder.build())); + if (!projectStage.isProductionStage()) { + DebugInfo.Builder debugInfoBuilder = toDebugInfo(exception); + result.addDetails(Any.pack(debugInfoBuilder.build())); + } + return result; + } +} diff --git a/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/mapper/IGrpcExceptionTranslator.java b/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/mapper/IGrpcExceptionTranslator.java new file mode 100644 index 000000000..dd068b4e0 --- /dev/null +++ b/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/mapper/IGrpcExceptionTranslator.java @@ -0,0 +1,67 @@ +/*- + * #%L + * Coffee + * %% + * Copyright (C) 2020 - 2024 i-Cell Mobilsoft Zrt. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package hu.icellmobilsoft.coffee.grpc.server.mapper; + +import java.io.Serializable; +import java.util.Locale; + +import com.google.rpc.LocalizedMessage; + +import hu.icellmobilsoft.coffee.se.api.exception.BaseException; + +/** + * Interface for translating exceptions to Grpc LocalizedMessage. + * + * @author Imre Scheffer + * @since 2.7.0 + */ +public interface IGrpcExceptionTranslator { + + /** + * Creates a {@link LocalizedMessage} from a given {@link BaseException}. + * + * @param locale + * The locale used following the specification defined at http://www.rfc-editor.org/rfc/bcp/bcp47.txt. Examples are: "en-US", "fr-CH", + * "es-MX". + * @param e + * {@link BaseException} to localize + * @return Translated Grpc {@link LocalizedMessage} from exception + */ + default LocalizedMessage.Builder toLocalizedMessage(Locale locale, BaseException e) { + if (e == null) { + return LocalizedMessage.newBuilder(); + } + return toLocalizedMessage(locale, e.getFaultTypeEnum()); + } + + /** + * Creates a {@link LocalizedMessage} from a given Enum. + * + * @param locale + * The locale used following the specification defined at http://www.rfc-editor.org/rfc/bcp/bcp47.txt. Examples are: "en-US", "fr-CH", + * "es-MX" + * @param faultType + * Enum to localize + * @param messageArguments + * Message arguments to fill faulType template with values + * @return Translated Grpc {@link LocalizedMessage} from enum + */ + LocalizedMessage.Builder toLocalizedMessage(Locale locale, Enum faultType, Serializable... messageArguments); +} diff --git a/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/metadata/GrpcHeaderHelper.java b/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/metadata/GrpcHeaderHelper.java new file mode 100644 index 000000000..c94e4decc --- /dev/null +++ b/coffee-grpc/coffee-grpc-server-extension/src/main/java/hu/icellmobilsoft/coffee/grpc/server/metadata/GrpcHeaderHelper.java @@ -0,0 +1,69 @@ +/*- + * #%L + * Coffee + * %% + * Copyright (C) 2020 - 2024 i-Cell Mobilsoft Zrt. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package hu.icellmobilsoft.coffee.grpc.server.metadata; + +import java.text.MessageFormat; +import java.util.Locale; + +import org.apache.commons.lang3.StringUtils; + +import hu.icellmobilsoft.coffee.grpc.api.metadata.IGrpcHeader; +import hu.icellmobilsoft.coffee.se.logging.Logger; +import io.grpc.Metadata; + +/** + * Grpc communication header helper + * + * @author Imre Scheffer + * @since 2.7.0 + */ +public class GrpcHeaderHelper { + + /** + * Default constructor, constructs a new object. + */ + private GrpcHeaderHelper() { + super(); + } + + /** + * Get Locale parameter from Grpc request metadata + * + * @param requestHeaders + * Grpc request metadata + * @return Locale from request + */ + public static Locale getRequestLocale(Metadata requestHeaders) { + if (requestHeaders == null) { + return null; + } + String languageString = requestHeaders.get(IGrpcHeader.HEADER_LANGUAGE); + if (StringUtils.isNotBlank(languageString)) { + try { + return new Locale(languageString); + } catch (Exception e) { + String msg = MessageFormat.format("Failed to determine Locale setting from language header [{0}], value [{1}]: [{2}]", + IGrpcHeader.HEADER_LANGUAGE.originalName(), languageString, e.getLocalizedMessage()); + Logger.getLogger(GrpcHeaderHelper.class).debug(msg, e); + } + } + return null; + } +} diff --git a/docs/en/common/core/coffee-grpc.adoc b/docs/en/common/core/coffee-grpc.adoc index 5c3e926cb..07c1bc2f1 100644 --- a/docs/en/common/core/coffee-grpc.adoc +++ b/docs/en/common/core/coffee-grpc.adoc @@ -52,6 +52,13 @@ Implemented features: * Request/Response log, applicable with <> annotation on GRPC service implementation method/class * Exception handling +** Grpc status code mapping +*** General exception: `hu.icellmobilsoft.coffee.grpc.server.mapper.GrpcGeneralExceptionMapper` +*** BaseException exception: `hu.icellmobilsoft.coffee.grpc.server.mapper.GrpcBaseExceptionMapper` +** Grpc header response additions: +*** Business error code (`com.google.rpc.ErrorInfo`) +*** Business error code translation by request locale (`com.google.rpc.LocalizedMessage`) +*** Debug informations (`com.google.rpc.DebugInfo`) === Server thread pool The thread handling is an important part of the gRPC server. Two solutions have been implemented: diff --git a/docs/en/migration/migration260to270.adoc b/docs/en/migration/migration260to270.adoc index b7fd5e5ed..e3529d601 100644 --- a/docs/en/migration/migration260to270.adoc +++ b/docs/en/migration/migration260to270.adoc @@ -96,6 +96,11 @@ which support anyDto <--> protoDto conversion * Improved `hu.icellmobilsoft.coffee.grpc.server.interceptor.ServerRequestInterceptor` and `hu.icellmobilsoft.coffee.grpc.server.interceptor.ServerResponseInterceptor` logging interceptors, which can be parameterized with `@LogSpecifiers` and `@LogSpecifier` annotations. +* Improved `hu.icellmobilsoft.coffee.grpc.server.interceptor.ErrorHandlerInterceptor` +now can handle additional error information into Grpc response: +** Business error code (FaultType) +** Translated error code +** Debug information (stacktrace) ==== Migration diff --git a/docs/hu/common/core/coffee-grpc.adoc b/docs/hu/common/core/coffee-grpc.adoc index 64eb8be4a..19872330d 100644 --- a/docs/hu/common/core/coffee-grpc.adoc +++ b/docs/hu/common/core/coffee-grpc.adoc @@ -57,7 +57,14 @@ Implementált funkciók: * MDC kezelés * Request/Response log, használható a <> annotációval a GRPC service method/class implementáción -* Exception kezelés +* Exception kezelés: +** Grpc status code mappelés +*** Általános hiba: `hu.icellmobilsoft.coffee.grpc.server.mapper.GrpcGeneralExceptionMapper` +*** BaseException hiba: `hu.icellmobilsoft.coffee.grpc.server.mapper.GrpcBaseExceptionMapper` +** Grpc header response dúsítás: +*** Üzleti hibakód (`com.google.rpc.ErrorInfo`) +*** Üzleti hibakód nyelvesítése request nyelvi kérése alapján (`com.google.rpc.LocalizedMessage`) +*** Debug információk (`com.google.rpc.DebugInfo`) === Szerver thread pool A gRPC server fontos része a szál kezelés. diff --git a/docs/hu/migration/migration260to270.adoc b/docs/hu/migration/migration260to270.adoc index 273b01bab..de0fe3a30 100644 --- a/docs/hu/migration/migration260to270.adoc +++ b/docs/hu/migration/migration260to270.adoc @@ -99,6 +99,11 @@ ami anyDto <--> protoDto konverziót támogatja * Feljavított `hu.icellmobilsoft.coffee.grpc.server.interceptor.ServerRequestInterceptor` és `hu.icellmobilsoft.coffee.grpc.server.interceptor.ServerResponseInterceptor` loggolási interceptor, melyeket paraméterezni lehet a `@LogSpecifiers` és `@LogSpecifier` annotációkkal +* Feljavított `hu.icellmobilsoft.coffee.grpc.server.interceptor.ErrorHandlerInterceptor` +mostmár kibővíti kiegészítő hiba információkkal a Grpc választ: +** Üzleti hibakód (FaultType) +** Nyelvesített hibakód +** Debug információ (stacktrace) ==== Átállás