ialbert 2022-08-17 16:18:01 +03:00
parent 0a9af63689
commit 6f6ace106d
29 changed files with 479 additions and 66 deletions

View file

@ -8,14 +8,13 @@ import java.util.Locale;
@Configuration
public class MessagesConfig {
@Bean
@Bean("validation-error-messages")
public ResourceBundleMessageSource messages() {
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
source.setBasenames("messages/response");
source.setBasenames("messages/error");
source.setUseCodeAsDefaultMessage(true);
source.setDefaultEncoding("utf8");
source.setDefaultLocale(Locale.ROOT);
return source;
}
}

View file

@ -1,95 +1,88 @@
package ru.spcex.clearing.backendapi.controller.cud;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.annotations.*;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import ru.spcex.clearing.backendapi.controller.response.BasicSpcexResponse;
import ru.spcex.clearing.backendapi.controller.response.cud.CudResponse;
import ru.spcex.clearing.backendapi.domain.actions.IAction;
import ru.spcex.clearing.backendapi.meta.CudMetaService;
import ru.spcex.clearing.backendapi.service.IOperator;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@Controller
@RequestMapping("/cud")
public class CudController {
private final Producer<String, Object> kafka;
private final IOperator operator;
private final ObjectMapper json;
private final CudMetaService meta;
@Autowired
public CudController(Producer<String, Object> kafka, CudMetaService meta) {
this.kafka = kafka;
public CudController(IOperator operator, CudMetaService meta) {
this.meta = meta;
this.json = new ObjectMapper();
this.operator = operator;
}
@ApiOperation(value = "create/update/delete business objects. See meta.xml for field descriptions.")
@ApiResponses(value = {@ApiResponse(code = 200, message = "OK", response = MetaDataResponse.class)})
@ApiResponses(value = {@ApiResponse(code = 200, message = "OK", response = CudResponse.class), @ApiResponse(code = 400, message = "Ошибка валидации", response = BasicSpcexResponse.class)})
@RequestMapping(value = "/{destination}", method = RequestMethod.POST)
@ResponseBody
public MetaDataResponse add(
public CudResponse add(
@ApiParam(value = "Последняя часть URL определяет 'направление', по которому пойдет запрос. " +
"Должно биться с форматом запроса.", required = true, example = "money-market-security-new")
@PathVariable("destination")
String destination,
@ApiParam(value = "Параметры команды в JSON формате, поля см. в meta.xml.", required = true)
@RequestBody String body) throws JsonProcessingException, ExecutionException, InterruptedException {
return processRequest(destination, body);
}
@ApiOperation(value = "create/update/delete business objects. See meta.xml for field descriptions.")
@ApiResponses(value = {@ApiResponse(code = 200, message = "OK", response = CudResponse.class), @ApiResponse(code = 400, message = "Ошибка валидации", response = BasicSpcexResponse.class)})
@RequestMapping(value = "/{destination}", method = RequestMethod.PUT)
@ResponseBody
public CudResponse update(
@ApiParam(value = "Последняя часть URL определяет 'направление', по которому пойдет запрос. " +
"Должно биться с форматом запроса.", required = true, example = "money-market-security-new")
@PathVariable("destination")
String destination,
@ApiParam(value = "Параметры команды в JSON формате, поля см. в meta.xml.", required = true)
@RequestBody String body) throws JsonProcessingException, ExecutionException, InterruptedException {
return processRequest(destination, body);
}
@ApiOperation(value = "delete business objects. See meta.xml for field descriptions.")
@ApiResponses(value = {@ApiResponse(code = 200, message = "OK", response = CudResponse.class), @ApiResponse(code = 400, message = "Ошибка валидации", response = BasicSpcexResponse.class)})
@RequestMapping(value = "/{destination}", method = RequestMethod.DELETE)
@ResponseBody
public CudResponse delete(
@ApiParam(value = "Последняя часть URL определяет 'направление', по которому пойдет запрос. " +
"Должно биться с форматом запроса.", required = true, example = "money-market-security-new")
@PathVariable("destination")
String destination,
@ApiParam(value = "Параметры команды в JSON формате, поля см. в meta.xml.", required = true)
@RequestBody String body) throws JsonProcessingException, ExecutionException, InterruptedException {
return processRequest(destination, body);
}
private CudResponse processRequest(String destination, String body) throws ExecutionException, InterruptedException, JsonProcessingException {
Class<IAction<?>> actionClazz = meta.byDestination(destination);
if (actionClazz == null) {
throw new UnsupportedOperationException("unsupported destination " + destination);
}
IAction<?> iAction = json.readValue(body, actionClazz);
Future<RecordMetadata> send = kafka.send(new ProducerRecord<>(destination, iAction.toRequest()));
RecordMetadata kafkaMetaData = send.get();
MetaDataResponse responseToClient = new MetaDataResponse();
responseToClient.setOffset(kafkaMetaData.offset());
responseToClient.setPartition(kafkaMetaData.partition());
responseToClient.setTopic(kafkaMetaData.topic());
CudResponse responseToClient = new CudResponse();
responseToClient.setResponse(operator.sendRequestToQueue(destination, iAction));
responseToClient.setCode(0);
responseToClient.setMessage("success");
return responseToClient;
}
@ApiModel(description="Ответ в результате отправки операции в топик Kafka.")
private static class MetaDataResponse extends BasicSpcexResponse {
@ApiModelProperty(value="Offset положенного сообщения")
@JsonProperty
private Long offset;
@ApiModelProperty(value="Partition положенного сообщения")
@JsonProperty
private Integer partition;
@ApiModelProperty(value="Название топика, в который было положено сообщение")
@JsonProperty
private String topic;
public Long getOffset() {
return offset;
}
public void setOffset(Long offset) {
this.offset = offset;
}
public Integer getPartition() {
return partition;
}
public void setPartition(Integer partition) {
this.partition = partition;
}
public String getTopic() {
return topic;
}
public void setTopic(String topic) {
this.topic = topic;
}
}
}

View file

@ -0,0 +1,43 @@
package ru.spcex.clearing.backendapi.controller.cud;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import ru.spcex.clearing.backendapi.controller.response.BasicSpcexResponse;
import ru.spcex.clearing.backendapi.errors.ActionValidationException;
import ru.spcex.platform.utils.enumeration.EnumMessage;
import ru.spcex.platform.utils.enumeration.IMessageResolver;
import ru.spcex.platform.utils.log.ExceptionUtils;
@ControllerAdvice("ru.spcex.clearing.backendapi.controller.cud")
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CudExceptionHandler extends ResponseEntityExceptionHandler {
private final Logger log = LoggerFactory.getLogger(getClass());
private final IMessageResolver errorResolver;
@Autowired
public CudExceptionHandler(@Qualifier("errorResolver") IMessageResolver errorResolver) {
this.errorResolver = errorResolver;
}
@ExceptionHandler(value = ActionValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public BasicSpcexResponse handleValidationException(ActionValidationException e) {
log.error("Default scoring exception handler: {}", ExceptionUtils.getStackTrace(e));
EnumMessage firstError = e.getErrors().iterator().next();
BasicSpcexResponse errorResponse = new BasicSpcexResponse();
errorResponse.setCode(firstError.getSubject().getId());
errorResponse.setMessage(errorResolver.resolve(firstError));
return errorResponse;
}
}

View file

@ -4,10 +4,15 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import ru.spcex.clearing.backendapi.domain.actions.IAction;
import ru.spcex.clearing.backendapi.errors.BackEndError;
import ru.spcex.clearing.platform.messaging.domain.cud.securitites.MoneyMarketSecurityNewRequest;
import ru.spcex.clearing.platform.messaging.domain.json.deserialize.InstantDeserializer;
import ru.spcex.platform.utils.enumeration.EnumMessage;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class MoneyMarketSecurityNewAction implements IAction<MoneyMarketSecurityNewRequest> {
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "Europe/Moscow")
@ -31,6 +36,13 @@ public class MoneyMarketSecurityNewAction implements IAction<MoneyMarketSecurity
@JsonProperty
public Double lotSize;
@Override
public Collection<EnumMessage> validate() {
if (this.startDate == null)
return List.of(new EnumMessage(BackEndError.ValidationError, "startDate"));
else return Collections.emptyList();
}
@Override
public MoneyMarketSecurityNewRequest toRequest() {
var req = new MoneyMarketSecurityNewRequest();

View file

@ -6,15 +6,15 @@ import io.swagger.annotations.ApiModelProperty;
@ApiModel(description="Базовый формат ответа")
public class BasicSpcexResponse {
@ApiModelProperty(value="Код ответа (успешный 0)", required = true)
private int code;
private long code;
@ApiModelProperty(value="Сообщение ответа")
private String message;
public int getCode() {
public long getCode() {
return code;
}
public void setCode(int code) {
public void setCode(long code) {
this.code = code;
}

View file

@ -0,0 +1,19 @@
package ru.spcex.clearing.backendapi.controller.response.cud;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import ru.spcex.clearing.backendapi.controller.response.BasicSpcexResponse;
@ApiModel(description = "Ответ в результате отправки операции в топик Kafka.")
public class CudResponse extends BasicSpcexResponse {
@ApiModelProperty(value = "Информация о принятом запросе")
QueueSuccessResponse response;
public QueueSuccessResponse getResponse() {
return response;
}
public void setResponse(QueueSuccessResponse response) {
this.response = response;
}
}

View file

@ -0,0 +1,27 @@
package ru.spcex.clearing.backendapi.controller.response.cud;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import ru.spcex.clearing.platform.messaging.domain.ActionType;
public class QueueSuccessResponse {
@ApiModelProperty(value = "Тип запроса")
@JsonProperty
private final ActionType action;
@ApiModelProperty(value = "Идентификатор запроса")
@JsonProperty
private final Long id;
public QueueSuccessResponse(ActionType action, Long id) {
this.action = action;
this.id = id;
}
public ActionType getAction() {
return action;
}
public Long getId() {
return id;
}
}

View file

@ -1,5 +1,12 @@
package ru.spcex.clearing.backendapi.domain.actions;
public interface IAction<T> {
public T toRequest();
import ru.spcex.clearing.platform.messaging.domain.BaseRequest;
import ru.spcex.platform.utils.enumeration.EnumMessage;
import java.util.Collection;
import java.util.Collections;
public interface IAction<T extends BaseRequest> {
T toRequest();
default Collection<EnumMessage> validate() {return Collections.emptyList();}
}

View file

@ -0,0 +1,22 @@
package ru.spcex.clearing.backendapi.errors;
import ru.spcex.platform.utils.enumeration.EnumMessage;
import java.util.Collection;
import java.util.List;
public class ActionValidationException extends RuntimeException {
protected Collection<EnumMessage> errors;
public ActionValidationException(Collection<EnumMessage> errors) {
this.errors = errors;
}
public ActionValidationException(EnumMessage error) {
this.errors = List.of(error);
}
public Collection<EnumMessage> getErrors() {
return errors;
}
}

View file

@ -0,0 +1,17 @@
package ru.spcex.clearing.backendapi.errors;
import ru.spcex.platform.utils.enumeration.IEnumId;
public enum BackEndError implements IEnumId {
ValidationError(3000L);
private final Long id;
BackEndError(Long id) {
this.id = id;
}
@Override
public Long getId() {
return id;
}
}

View file

@ -0,0 +1,28 @@
package ru.spcex.clearing.backendapi.errors;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.stereotype.Service;
import ru.spcex.platform.utils.enumeration.EnumMessage;
import ru.spcex.platform.utils.enumeration.IMessageResolver;
import java.util.Locale;
@Service("errorResolver")
public class ErrorResolver implements IMessageResolver {
private final ResourceBundleMessageSource errorMessages;
public ErrorResolver(ResourceBundleMessageSource errorMessages) {
this.errorMessages = errorMessages;
}
@Override
public String resolve(EnumMessage errorMessage) {
Long id = errorMessage.getSubject().getId();
String messageTemplate = errorMessages.getMessage(id.toString(), null, Locale.ENGLISH);
if (errorMessage.getArgs().length > 0)
return String.format(messageTemplate, errorMessage.getArgs());
else {
return messageTemplate;
}
}
}

View file

@ -0,0 +1,10 @@
package ru.spcex.clearing.backendapi.service;
import ru.spcex.clearing.backendapi.controller.response.cud.QueueSuccessResponse;
import ru.spcex.clearing.backendapi.domain.actions.IAction;
import java.util.concurrent.ExecutionException;
public interface IOperator {
QueueSuccessResponse sendRequestToQueue(String destination, IAction<?> iAction) throws ExecutionException, InterruptedException;
}

View file

@ -0,0 +1,46 @@
package ru.spcex.clearing.backendapi.service.impl;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.springframework.stereotype.Service;
import ru.spcex.clearing.backendapi.controller.response.cud.QueueSuccessResponse;
import ru.spcex.clearing.backendapi.domain.actions.IAction;
import ru.spcex.clearing.backendapi.errors.ActionValidationException;
import ru.spcex.clearing.backendapi.service.IOperator;
import ru.spcex.clearing.platform.messaging.domain.BaseRequest;
import ru.spcex.platform.imdg.api.ImdgId;
import ru.spcex.platform.imdg.api.ImdgProvider;
import ru.spcex.platform.utils.enumeration.EnumMessage;
import java.util.Collection;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@Service
public class OperatorImpl implements IOperator {
private final Producer<String, Object> kafka;
private final ImdgId idGenerator;
public OperatorImpl(Producer<String, Object> kafka, ImdgProvider imdgProvider) {
this.kafka = kafka;
this.idGenerator = imdgProvider.getImdgIdGenerator();
}
@Override
public QueueSuccessResponse sendRequestToQueue(String destination, IAction<?> iAction) throws ExecutionException, InterruptedException {
throwValidate(iAction);
BaseRequest request = iAction.toRequest();
request.setId(idGenerator.nextId());
Future<RecordMetadata> send = kafka.send(new ProducerRecord<>(destination, request));
send.get();
return new QueueSuccessResponse(request.getActionType(), request.getId());
}
private void throwValidate(IAction<?> iAction) {
Collection<EnumMessage> validationErrors = iAction.validate();
if (validationErrors.size() > 0) {
throw new ActionValidationException(validationErrors);
}
}
}

View file

@ -0,0 +1,2 @@
1=This is error example 1.
3000=Validation error field: %s

View file

@ -0,0 +1,2 @@
1=Пример ошибки с идентификатором 1.
3000=Ошибка валидации, поле: %s

View file

@ -1 +0,0 @@
backend-api.response.test1=This is response example 1.

View file

@ -1 +0,0 @@
backend-api.response.test1=Это пример ответа 1.

View file

@ -0,0 +1,21 @@
package ru.spcex.platform.imdg.iml.hazelcast.adapter;
import com.hazelcast.core.IdGenerator;
import ru.spcex.platform.imdg.api.ImdgId;
public class ImdgIdGeneratorHazelcast implements ImdgId {
private IdGenerator idGenerator;
public IdGenerator getIdGenerator() {
return idGenerator;
}
public void setIdGenerator(IdGenerator idGenerator) {
this.idGenerator = idGenerator;
}
@Override
public Long nextId() {
return idGenerator.newId();
}
}

View file

@ -12,8 +12,10 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import ru.spcex.clearing.imdg.IMDGDistributedNames;
import ru.spcex.platform.classes.base.SpcexObjectBase;
import ru.spcex.platform.imdg.api.Imdg;
import ru.spcex.platform.imdg.api.ImdgId;
import ru.spcex.platform.imdg.api.ImdgProvider;
import ru.spcex.platform.imdg.iml.hazelcast.adapter.ImdgHazelcast;
import ru.spcex.platform.imdg.iml.hazelcast.adapter.ImdgIdGeneratorHazelcast;
import ru.spcex.platform.imdg.iml.hazelcast.config.HazelcastClientParams;
import ru.spcex.platform.imdg.iml.hazelcast.util.HazelcastHelper;
import ru.spcex.platform.utils.log.ExceptionUtils;
@ -231,6 +233,23 @@ public abstract class HazelcastServiceBase
return imdg;
}
@Override
public ImdgId getImdgIdGenerator() {
ImdgIdGeneratorHazelcast imdgId = new ImdgIdGeneratorHazelcast();
statusSubscribe(new IHazelcastClusterStatus() {
@Override
public void getAvailable(HazelcastInstance hazelcastNotInited) {
IdGenerator generator = hazelcastInstance.getIdGenerator(IMDGDistributedNames.MAP_SEQUENCE_NAME);
imdgId.setIdGenerator(generator);
}
@Override
public void getUnavailable(HazelcastInstance hazelcastNotInited) {
}
});
return imdgId;
}
// @Override
public void waitTillReadyState() throws InterruptedException {
boolean done = false;

View file

@ -0,0 +1,5 @@
package ru.spcex.platform.imdg.api;
public interface ImdgId {
Long nextId();
}

View file

@ -8,4 +8,6 @@ public interface ImdgProvider {
*/
public <T extends SpcexObjectBase> Imdg<T> getImdg(String key, Class<T> clazz);
public ImdgId getImdgIdGenerator();
}

View file

@ -39,6 +39,11 @@
<artifactId>platform-utils</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>ru.spcex.platform</groupId>
<artifactId>platform-classes-base</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,5 @@
package ru.spcex.clearing.platform.messaging.domain;
public enum ActionType {
NEW
}

View file

@ -0,0 +1,8 @@
package ru.spcex.clearing.platform.messaging.domain;
import ru.spcex.platform.classes.base.interfaces.WithId;
public interface BaseRequest extends WithId {
void setId(Long id);
ActionType getActionType();
}

View file

@ -3,12 +3,16 @@ package ru.spcex.clearing.platform.messaging.domain.cud.securitites;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import ru.spcex.clearing.platform.messaging.domain.ActionType;
import ru.spcex.clearing.platform.messaging.domain.BaseRequest;
import ru.spcex.clearing.platform.messaging.domain.json.deserialize.InstantDeserializer;
import ru.spcex.clearing.platform.messaging.domain.json.serialize.InstantSerializer;
import java.time.Instant;
public class MoneyMarketSecurityNewRequest {
public class MoneyMarketSecurityNewRequest implements BaseRequest {
@JsonProperty
public Long id;
@JsonProperty
@JsonSerialize(using = InstantSerializer.class)
@JsonDeserialize(using = InstantDeserializer.class)
@ -30,6 +34,21 @@ public class MoneyMarketSecurityNewRequest {
@JsonProperty
public Double lotSize;
@Override
public Long getId() {
return id;
}
@Override
public ActionType getActionType() {
return ActionType.NEW;
}
@Override
public void setId(Long id) {
this.id = id;
}
public Instant getStartDate() {
return startDate;
}

View file

@ -0,0 +1,35 @@
package ru.spcex.platform.utils.enumeration;
import java.util.Collections;
public class EnumMessage {
private final IEnumId subject;
/**
* Параметры для подстановки в текст сообщения. Все Serializable.
* args not null.
*/
private Object[] args;
public EnumMessage(IEnumId subject) {
this.subject = subject;
this.args = Collections.emptyList().toArray(new Object[0]);
}
public EnumMessage(IEnumId subject, Object... args) {
this.subject = subject;
this.args = args;
}
public IEnumId getSubject() {
return subject;
}
public Object[] getArgs() {
return args;
}
public void setArgs(Object[] args) {
this.args = args;
}
}

View file

@ -0,0 +1,64 @@
package ru.spcex.platform.utils.enumeration;
import java.io.Serializable;
import java.util.Objects;
public interface IEnumId extends Serializable {
Long getId();
default boolean equalsById(Long id) {
return id != null && getId().equals(id);
}
/**
* Проверяет что среди данного набора Enum, присутствует элемент с данным id
* id может быть null
*/
static <T extends Enum<T> & IEnumId> boolean contains(java.lang.Long id, T... enumSet) {
for (T e : enumSet) {
if (Objects.equals(id, e.getId()))
return true;
}
return false;
}
static <T extends Enum<T> & IEnumId> boolean contains(T e, T... enumSet) {
for (T enumEl : enumSet) {
if (enumEl.equals(e))
return true;
}
return false;
}
/**
* Проверяет что среди всех Enum данного класса, присутствует элемент с данным id
* id может быть null
*/
static <T extends Enum<T> & IEnumId> boolean contains(Class<T> enumClass, Long id) {
for (T e : enumClass.getEnumConstants()) {
if (Objects.equals(id, e.getId()))
return true;
}
return false;
}
/**
* Возвращает Enum по id, если в заданном классе такой определен
* id может быть null
*
* @return Enum если нашел, иначе <tt>null</tt>
*/
static <T extends Enum<T> & IEnumId> T getEnumById(Class<T> enumClass, Long id) {
for (T e : enumClass.getEnumConstants()) {
if (Objects.equals(id, e.getId()))
return e;
}
return null;
}
static Long getIdOrNull(IEnumId enumVal) {
return enumVal == null ? null : enumVal.getId();
}
Long UNDEFINED_VALUE = Long.MIN_VALUE;
}

View file

@ -0,0 +1,5 @@
package ru.spcex.platform.utils.enumeration;
public interface IMessageResolver {
String resolve(EnumMessage errorMessage);
}

View file

@ -1,3 +1,3 @@
#!/bin/sh
docker build -t securities-service:1.0.0 ../modules/securities-service/
docker run -p 8050:8080 -v /opt/clearing/logs:/opt/clearing/bin/logs securities-service:1.0.0 &
docker run -v /opt/clearing/logs:/opt/clearing/bin/logs securities-service:1.0.0 &