Added MonitoringRequest entity; Added validation for MonitoringRequest; Added unmarshalling for XmlProcessor; Added unmarshalling for MonitoringRequest

This commit is contained in:
Mikhail Trofimov 2025-06-05 12:50:48 +03:00
parent 97cdefd166
commit ace8bd46e4
12 changed files with 326 additions and 32 deletions

65
monitoring-request.xsd Normal file
View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="MonitoringRequest">
<xs:complexType>
<xs:sequence>
<xs:element name="UserId" type="RequiredString"/>
<xs:element name="Password" type="RequiredString"/>
<xs:element name="PackageName" type="PackageNameType"/>
<xs:element name="Subjects">
<xs:complexType>
<xs:sequence>
<xs:element name="Subject" type="Subject" minOccurs="1" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<!-- Проверка уникальности Id в рамках всего XML -->
<xs:unique name="uniqueSubjectId">
<xs:selector xpath=".//Subject"/>
<xs:field xpath="Id"/>
</xs:unique>
</xs:element>
<!-- Тип для Subject -->
<xs:complexType name="Subject">
<xs:sequence>
<xs:element name="Id" type="IdType"/>
<xs:element name="ClientId" type="ClientIdType"/>
</xs:sequence>
</xs:complexType>
<!-- Тип для ClientId (ровно 9 цифр) -->
<xs:simpleType name="ClientIdType">
<xs:restriction base="xs:string">
<xs:length value="9"/>
<xs:pattern value="[0-9]{9}"/>
</xs:restriction>
</xs:simpleType>
<!-- Тип для Id (не более 36 символов) -->
<xs:simpleType name="IdType">
<xs:restriction base="xs:string">
<xs:maxLength value="36"/>
</xs:restriction>
</xs:simpleType>
<!-- Тип для PackageName -->
<xs:simpleType name="PackageNameType">
<xs:restriction base="xs:string">
<xs:maxLength value="22"/>
<xs:pattern value="[A-Z0-9_]+"/>
</xs:restriction>
</xs:simpleType>
<!-- Базовый тип для обязательных строковых полей -->
<xs:simpleType name="RequiredString">
<xs:restriction base="xs:string">
<xs:minLength value="1"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>

View file

@ -17,4 +17,6 @@ public class CreditTrackerSettings {
private DataSourceSettings signalDataSourceSettings; private DataSourceSettings signalDataSourceSettings;
@NestedConfigurationProperty @NestedConfigurationProperty
private DataSourceSettings scoringDataSourceSettings; private DataSourceSettings scoringDataSourceSettings;
@NestedConfigurationProperty
private XsdValidation xsdValidation;
} }

View file

@ -0,0 +1,5 @@
package ru.nbch.credit_tracker.config.settings;
public record XsdValidation (
String monitoringRequestPath
) {}

View file

@ -0,0 +1,31 @@
package ru.nbch.credit_tracker.entities.monitoring_request;
import jakarta.xml.bind.annotation.*;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
"userId",
"password",
"packageName",
"subjects"
})
@XmlRootElement(name = "MonitoringRequest")
public class MonitoringRequest {
@XmlElement(name = "UserId", required = true)
protected String userId;
@XmlElement(name = "Password", required = true)
protected String password;
@XmlElement(name = "PackageName", required = true)
protected String packageName;
@XmlElement(name = "Subjects", required = true)
protected Subjects subjects;
}

View file

@ -0,0 +1,28 @@
package ru.nbch.credit_tracker.entities.monitoring_request;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlType;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "SubjectType", propOrder = {
"id",
"clientId"
})
public class Subject {
@XmlElement(name = "Id", required = true)
protected String id;
@XmlElement(name = "ClientId", required = true)
protected String clientId;
}

View file

@ -0,0 +1,27 @@
package ru.nbch.credit_tracker.entities.monitoring_request;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlType;
import java.util.ArrayList;
import java.util.List;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
"subject"
})
public class Subjects {
@XmlElement(name = "Subject", required = true)
protected List<Subject> subject;
public List<Subject> getSubject() {
if (subject == null) {
subject = new ArrayList<>();
}
return this.subject;
}
}

View file

@ -0,0 +1,17 @@
package ru.nbch.credit_tracker.exceptions;
import lombok.Getter;
@Getter
public class ValidationException extends Exception {
private String elementName;
public ValidationException() {
}
public ValidationException(String elementName) {
super();
this.elementName = elementName;
}
}

View file

@ -1,19 +1,20 @@
package ru.nbch.credit_tracker.facade; package ru.nbch.credit_tracker.facade;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.PathResource; import org.springframework.core.io.PathResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.entities.monitoring_request.MonitoringRequest;
import ru.nbch.credit_tracker.enums.SignalError; import ru.nbch.credit_tracker.enums.SignalError;
import ru.nbch.credit_tracker.exceptions.ValidationException;
import ru.nbch.credit_tracker.service.FailureAnswerGenerator; import ru.nbch.credit_tracker.service.FailureAnswerGenerator;
import ru.nbch.credit_tracker.service.scoring.AuthorizationService; import ru.nbch.credit_tracker.service.scoring.AuthorizationService;
import ru.nbch.credit_tracker.service.xml.IXmlProcessor;
import ru.nbch.credit_tracker.service.xml.XmlUtilService;
import java.io.IOException;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
@ -22,16 +23,23 @@ import java.util.zip.ZipInputStream;
public class SignalFacade { public class SignalFacade {
private static final long MAX_XML_FILE_SIZE = 2L * 1024 * 1024 * 1024; private static final long MAX_XML_FILE_SIZE = 2L * 1024 * 1024 * 1024;
private final CreditTrackerSettings config;
private final AuthorizationService authorizationService; private final AuthorizationService authorizationService;
private final FailureAnswerGenerator failureAnswerGenerator; private final FailureAnswerGenerator failureAnswerGenerator;
private final XmlUtilService xmlUtilService;
public SignalFacade(AuthorizationService authorizationService, public SignalFacade(CreditTrackerSettings config,
FailureAnswerGenerator failureAnswerGenerator) { AuthorizationService authorizationService,
FailureAnswerGenerator failureAnswerGenerator,
XmlUtilService xmlUtilService) {
this.config = config;
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
this.failureAnswerGenerator = failureAnswerGenerator; this.failureAnswerGenerator = failureAnswerGenerator;
this.xmlUtilService = xmlUtilService;
} }
public ResponseEntity<Resource> setupMonitoring(MultipartFile file) { public ResponseEntity<Resource> setupMonitoring(MultipartFile file) {
// первичные валидации
if (file.isEmpty()) { if (file.isEmpty()) {
return getBadResponse(SignalError.ERROR_001); return getBadResponse(SignalError.ERROR_001);
} }
@ -40,6 +48,7 @@ public class SignalFacade {
return getBadResponse(SignalError.ERROR_001); return getBadResponse(SignalError.ERROR_001);
} }
// Валидация Архива на количетсво файлов, размер и расширение
try (ZipInputStream zipInputStream = new ZipInputStream(file.getInputStream())) { try (ZipInputStream zipInputStream = new ZipInputStream(file.getInputStream())) {
int fileCount = 0; int fileCount = 0;
ZipEntry xmlEntry = null; ZipEntry xmlEntry = null;
@ -72,14 +81,36 @@ public class SignalFacade {
return getBadResponse(SignalError.ERROR_009); return getBadResponse(SignalError.ERROR_009);
} }
} catch (Exception e) {
log.error(e.getMessage(), e);
String errorPath = failureAnswerGenerator.generateFailureAnswer(SignalError.ERROR_001);
return ResponseEntity.internalServerError().body(new PathResource(errorPath));
}
AuthInfoData authInfoData = parseAuthInfo(zipInputStream); // Теперь знаем, что в архиве лежит ровно один файл с раширением xml и он весит меньше 2гб. Можно валидировать сам xml
if (!authorizationService.hasAuthorization(authInfoData.getUserId(), authInfoData.getPassword())) { try (ZipInputStream zipInputStream = new ZipInputStream(file.getInputStream())) {
zipInputStream.getNextEntry();
validateRequestByXsd(zipInputStream);
} catch (ValidationException e) {
return getBadResponse(SignalError.ERROR_011); // TODO вычленять конкретный элемент
} catch (Exception e) {
log.error(e.getMessage(), e);
String errorPath = failureAnswerGenerator.generateFailureAnswer(SignalError.ERROR_001);
return ResponseEntity.internalServerError().body(new PathResource(errorPath));
}
// Xml валиден. Производим анмаршаллинг
try (ZipInputStream zipInputStream = new ZipInputStream(file.getInputStream())) {
zipInputStream.getNextEntry();
IXmlProcessor processor = xmlUtilService.createJaxbProcessor();
MonitoringRequest monitoringRequest = processor.read(zipInputStream, MonitoringRequest.class);
if (!authorizationService.hasAuthorization(monitoringRequest.getUserId(), monitoringRequest.getPassword())) {
return getBadResponse(SignalError.ERROR_005); return getBadResponse(SignalError.ERROR_005);
} }
// TODO process xml file
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
String errorPath = failureAnswerGenerator.generateFailureAnswer(SignalError.ERROR_001); String errorPath = failureAnswerGenerator.generateFailureAnswer(SignalError.ERROR_001);
@ -89,6 +120,13 @@ public class SignalFacade {
return ResponseEntity.ok().body(new PathResource("D:\\folder_name\\legal.xml")); // TODO generate success response return ResponseEntity.ok().body(new PathResource("D:\\folder_name\\legal.xml")); // TODO generate success response
} }
private void validateRequestByXsd(ZipInputStream inputStream) throws ValidationException {
try {
xmlUtilService.validateXMLByXSD(inputStream, config.getXsdValidation().monitoringRequestPath());
} catch (Exception e) {
throw new ValidationException();
}
}
private boolean isZipFile(MultipartFile file) { private boolean isZipFile(MultipartFile file) {
return file.getContentType() != null && return file.getContentType() != null &&
@ -96,21 +134,8 @@ public class SignalFacade {
file.getOriginalFilename() != null && file.getOriginalFilename().toLowerCase().endsWith(".zip"); file.getOriginalFilename() != null && file.getOriginalFilename().toLowerCase().endsWith(".zip");
} }
private AuthInfoData parseAuthInfo(ZipInputStream zipInputStream) throws IOException {
return new AuthInfoData("0001XX000001", "test"); // TODO parse
}
private ResponseEntity<Resource> getBadResponse(SignalError error) { private ResponseEntity<Resource> getBadResponse(SignalError error) {
String errorPath = failureAnswerGenerator.generateFailureAnswer(error); String errorPath = failureAnswerGenerator.generateFailureAnswer(error);
return ResponseEntity.badRequest().body(new PathResource(errorPath)); return ResponseEntity.badRequest().body(new PathResource(errorPath));
} }
@Getter
@Setter
@AllArgsConstructor
private static class AuthInfoData {
private String userId;
private String password;
}
} }

View file

@ -1,7 +1,16 @@
package ru.nbch.credit_tracker.service.xml; package ru.nbch.credit_tracker.service.xml;
import java.io.InputStream;
import java.nio.file.Path;
public interface IXmlProcessor { public interface IXmlProcessor {
String asString(Object xmlObj, boolean useStandalone); String asString(Object xmlObj, boolean useStandalone);
void write(String path, Object xmlObj, boolean useStandalone); void write(String path, Object xmlObj, boolean useStandalone);
<Z> Z read(InputStream inputStream, Class<Z> clazz);
<Z> Z read(Path xml, Class<Z> clazz);
<Z> Z read(String xml, Class<Z> clazz);
} }

View file

@ -3,24 +3,32 @@ package ru.nbch.credit_tracker.service.xml;
import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller; import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.Unmarshaller;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.eclipse.persistence.jaxb.MarshallerProperties; import org.eclipse.persistence.jaxb.MarshallerProperties;
import ru.nbch.credit_tracker.utils.EscapeHandlerUtil; import ru.nbch.credit_tracker.utils.EscapeHandlerUtil;
import java.io.FileOutputStream; import javax.xml.stream.XMLInputFactory;
import java.io.OutputStream; import javax.xml.stream.XMLStreamReader;
import java.io.StringWriter; import javax.xml.validation.Schema;
import java.io.*;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
@Slf4j @Slf4j
public abstract class XmlProcessorImplBase implements IXmlProcessor { public abstract class XmlProcessorImplBase implements IXmlProcessor {
protected String charsetName; private String charsetName;
private XMLInputFactory xmlInputFactory;
private Map<Class<?>, Schema> schemasMap = new HashMap<>();
public XmlProcessorImplBase() {
this.charsetName = "windows-1251";
}
public XmlProcessorImplBase(String charsetName) { public XmlProcessorImplBase(String charsetName) {
this(); this.xmlInputFactory = XMLInputFactory.newInstance();
this.xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
this.xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
this.charsetName = charsetName; this.charsetName = charsetName;
} }
@ -52,6 +60,39 @@ public abstract class XmlProcessorImplBase implements IXmlProcessor {
} }
} }
@Override
public <Z> Z read(InputStream inputStream, Class<Z> clazz) {
try {
XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(inputStream);
return genUnmarshaller(clazz).unmarshal(xmlStreamReader, clazz).getValue();
} catch (Exception e) {
log.error(ExceptionUtils.getStackTrace(e));
return null;
}
}
@Override
public <Z> Z read(Path xml, Class<Z> clazz) {
try {
XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(new FileReader(xml.toFile()));
return genUnmarshaller(clazz).unmarshal(xmlStreamReader, clazz).getValue();
} catch (Exception e) {
log.error(ExceptionUtils.getStackTrace(e));
return null;
}
}
@Override
public <Z> Z read(String xml, Class<Z> clazz) {
try {
XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(new StringReader(xml));
return genUnmarshaller(clazz).unmarshal(xmlStreamReader, clazz).getValue();
} catch (Exception e) {
log.error(ExceptionUtils.getStackTrace(e));
return null;
}
}
public Marshaller genMarshaller(Object obj, boolean useStandalone) throws JAXBException { public Marshaller genMarshaller(Object obj, boolean useStandalone) throws JAXBException {
Marshaller marshaller = getContextByObject(obj).createMarshaller(); Marshaller marshaller = getContextByObject(obj).createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
@ -66,6 +107,15 @@ public abstract class XmlProcessorImplBase implements IXmlProcessor {
return marshaller; return marshaller;
} }
private <G> Unmarshaller genUnmarshaller(Class<G> clazz) throws JAXBException {
Unmarshaller unmarshaller = getContextByClass(clazz).createUnmarshaller();
Schema schema;
if (schemasMap != null && (schema = schemasMap.get(clazz)) != null) {
unmarshaller.setSchema(schema);
}
return unmarshaller;
}
protected JAXBContext getContextByObject(Object obj) throws JAXBException { protected JAXBContext getContextByObject(Object obj) throws JAXBException {
Class<?> aClass = obj.getClass(); Class<?> aClass = obj.getClass();
return getContextByClass(aClass); return getContextByClass(aClass);

View file

@ -2,16 +2,48 @@ package ru.nbch.credit_tracker.service.xml;
import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.JAXBException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.xml.sax.SAXException;
import ru.nbch.credit_tracker.entities.monitoring_request.MonitoringRequest;
import ru.nbch.credit_tracker.entities.response.errors.MonitoringError; import ru.nbch.credit_tracker.entities.response.errors.MonitoringError;
import javax.xml.XMLConstants;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.stax.StAXSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import static javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI;
@Service @Service
public class XmlUtilService { public class XmlUtilService {
public IXmlProcessor createJaxbProcessor() throws JAXBException { public IXmlProcessor createJaxbProcessor() throws JAXBException {
return new XmlProcessorImplAnyClasses(Charset.forName("windows-1251"), return new XmlProcessorImplAnyClasses(Charset.forName("windows-1251"),
// unmarshalling classes
MonitoringRequest.class,
// marshalling classes
MonitoringError.class); MonitoringError.class);
} }
public void validateXMLByXSD(InputStream xmlStream, String xsdPath) throws IOException, XMLStreamException, SAXException {
SchemaFactory sf = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI);
Schema schema = sf.newSchema(new File(xsdPath));
XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
xmlInputFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
xmlInputFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
Validator validator = schema.newValidator();
XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(xmlStream);
validator.validate(new StAXSource(xmlStreamReader));
}
} }

View file

@ -23,3 +23,6 @@ ct.scoring-data-source-settings.password=tester
# TODO Increase file size limit if needed # TODO Increase file size limit if needed
#spring.servlet.multipart.max-file-size=10MB #spring.servlet.multipart.max-file-size=10MB
#spring.servlet.multipart.max-request-size=10MB #spring.servlet.multipart.max-request-size=10MB
# xsd paths
ct.xsd-validation.monitoring-request-path=monitoring-request.xsd