diff --git a/monitoring-request.xsd b/monitoring-request.xsd new file mode 100644 index 0000000..e89d9f2 --- /dev/null +++ b/monitoring-request.xsd @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/ru/nbch/credit_tracker/config/settings/CreditTrackerSettings.java b/src/main/java/ru/nbch/credit_tracker/config/settings/CreditTrackerSettings.java index 71c1a1b..146d130 100644 --- a/src/main/java/ru/nbch/credit_tracker/config/settings/CreditTrackerSettings.java +++ b/src/main/java/ru/nbch/credit_tracker/config/settings/CreditTrackerSettings.java @@ -17,4 +17,6 @@ public class CreditTrackerSettings { private DataSourceSettings signalDataSourceSettings; @NestedConfigurationProperty private DataSourceSettings scoringDataSourceSettings; + @NestedConfigurationProperty + private XsdValidation xsdValidation; } diff --git a/src/main/java/ru/nbch/credit_tracker/config/settings/XsdValidation.java b/src/main/java/ru/nbch/credit_tracker/config/settings/XsdValidation.java new file mode 100644 index 0000000..5749fa6 --- /dev/null +++ b/src/main/java/ru/nbch/credit_tracker/config/settings/XsdValidation.java @@ -0,0 +1,5 @@ +package ru.nbch.credit_tracker.config.settings; + +public record XsdValidation ( + String monitoringRequestPath +) {} diff --git a/src/main/java/ru/nbch/credit_tracker/entities/monitoring_request/MonitoringRequest.java b/src/main/java/ru/nbch/credit_tracker/entities/monitoring_request/MonitoringRequest.java new file mode 100644 index 0000000..fba8663 --- /dev/null +++ b/src/main/java/ru/nbch/credit_tracker/entities/monitoring_request/MonitoringRequest.java @@ -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; + + +} diff --git a/src/main/java/ru/nbch/credit_tracker/entities/monitoring_request/Subject.java b/src/main/java/ru/nbch/credit_tracker/entities/monitoring_request/Subject.java new file mode 100644 index 0000000..0c42a01 --- /dev/null +++ b/src/main/java/ru/nbch/credit_tracker/entities/monitoring_request/Subject.java @@ -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; + + +} diff --git a/src/main/java/ru/nbch/credit_tracker/entities/monitoring_request/Subjects.java b/src/main/java/ru/nbch/credit_tracker/entities/monitoring_request/Subjects.java new file mode 100644 index 0000000..bdd68a8 --- /dev/null +++ b/src/main/java/ru/nbch/credit_tracker/entities/monitoring_request/Subjects.java @@ -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; + + public List getSubject() { + if (subject == null) { + subject = new ArrayList<>(); + } + return this.subject; + } + +} diff --git a/src/main/java/ru/nbch/credit_tracker/exceptions/ValidationException.java b/src/main/java/ru/nbch/credit_tracker/exceptions/ValidationException.java new file mode 100644 index 0000000..edefe8b --- /dev/null +++ b/src/main/java/ru/nbch/credit_tracker/exceptions/ValidationException.java @@ -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; + } +} diff --git a/src/main/java/ru/nbch/credit_tracker/facade/SignalFacade.java b/src/main/java/ru/nbch/credit_tracker/facade/SignalFacade.java index 5d46661..6e5a49d 100644 --- a/src/main/java/ru/nbch/credit_tracker/facade/SignalFacade.java +++ b/src/main/java/ru/nbch/credit_tracker/facade/SignalFacade.java @@ -1,19 +1,20 @@ package ru.nbch.credit_tracker.facade; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.PathResource; import org.springframework.core.io.Resource; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; 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.exceptions.ValidationException; import ru.nbch.credit_tracker.service.FailureAnswerGenerator; 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.ZipInputStream; @@ -22,16 +23,23 @@ import java.util.zip.ZipInputStream; public class SignalFacade { private static final long MAX_XML_FILE_SIZE = 2L * 1024 * 1024 * 1024; + private final CreditTrackerSettings config; private final AuthorizationService authorizationService; private final FailureAnswerGenerator failureAnswerGenerator; + private final XmlUtilService xmlUtilService; - public SignalFacade(AuthorizationService authorizationService, - FailureAnswerGenerator failureAnswerGenerator) { + public SignalFacade(CreditTrackerSettings config, + AuthorizationService authorizationService, + FailureAnswerGenerator failureAnswerGenerator, + XmlUtilService xmlUtilService) { + this.config = config; this.authorizationService = authorizationService; this.failureAnswerGenerator = failureAnswerGenerator; + this.xmlUtilService = xmlUtilService; } public ResponseEntity setupMonitoring(MultipartFile file) { + // первичные валидации if (file.isEmpty()) { return getBadResponse(SignalError.ERROR_001); } @@ -40,6 +48,7 @@ public class SignalFacade { return getBadResponse(SignalError.ERROR_001); } + // Валидация Архива на количетсво файлов, размер и расширение try (ZipInputStream zipInputStream = new ZipInputStream(file.getInputStream())) { int fileCount = 0; ZipEntry xmlEntry = null; @@ -72,14 +81,36 @@ public class SignalFacade { 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); - if (!authorizationService.hasAuthorization(authInfoData.getUserId(), authInfoData.getPassword())) { + // Теперь знаем, что в архиве лежит ровно один файл с раширением xml и он весит меньше 2гб. Можно валидировать сам xml + 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); } - // TODO process xml file - } catch (Exception e) { log.error(e.getMessage(), e); 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 } + 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) { return file.getContentType() != null && @@ -96,21 +134,8 @@ public class SignalFacade { file.getOriginalFilename() != null && file.getOriginalFilename().toLowerCase().endsWith(".zip"); } - private AuthInfoData parseAuthInfo(ZipInputStream zipInputStream) throws IOException { - return new AuthInfoData("0001XX000001", "test"); // TODO parse - } - - private ResponseEntity getBadResponse(SignalError error) { String errorPath = failureAnswerGenerator.generateFailureAnswer(error); return ResponseEntity.badRequest().body(new PathResource(errorPath)); } - - @Getter - @Setter - @AllArgsConstructor - private static class AuthInfoData { - private String userId; - private String password; - } } diff --git a/src/main/java/ru/nbch/credit_tracker/service/xml/IXmlProcessor.java b/src/main/java/ru/nbch/credit_tracker/service/xml/IXmlProcessor.java index 8145598..cb29054 100644 --- a/src/main/java/ru/nbch/credit_tracker/service/xml/IXmlProcessor.java +++ b/src/main/java/ru/nbch/credit_tracker/service/xml/IXmlProcessor.java @@ -1,7 +1,16 @@ package ru.nbch.credit_tracker.service.xml; +import java.io.InputStream; +import java.nio.file.Path; + public interface IXmlProcessor { String asString(Object xmlObj, boolean useStandalone); void write(String path, Object xmlObj, boolean useStandalone); + + Z read(InputStream inputStream, Class clazz); + + Z read(Path xml, Class clazz); + + Z read(String xml, Class clazz); } diff --git a/src/main/java/ru/nbch/credit_tracker/service/xml/XmlProcessorImplBase.java b/src/main/java/ru/nbch/credit_tracker/service/xml/XmlProcessorImplBase.java index 227a9c9..bf11863 100644 --- a/src/main/java/ru/nbch/credit_tracker/service/xml/XmlProcessorImplBase.java +++ b/src/main/java/ru/nbch/credit_tracker/service/xml/XmlProcessorImplBase.java @@ -3,24 +3,32 @@ package ru.nbch.credit_tracker.service.xml; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Marshaller; +import jakarta.xml.bind.Unmarshaller; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.eclipse.persistence.jaxb.MarshallerProperties; import ru.nbch.credit_tracker.utils.EscapeHandlerUtil; -import java.io.FileOutputStream; -import java.io.OutputStream; -import java.io.StringWriter; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamReader; +import javax.xml.validation.Schema; +import java.io.*; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; @Slf4j public abstract class XmlProcessorImplBase implements IXmlProcessor { - protected String charsetName; + private String charsetName; + private XMLInputFactory xmlInputFactory; + private Map, Schema> schemasMap = new HashMap<>(); - public XmlProcessorImplBase() { - this.charsetName = "windows-1251"; - } 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; } @@ -52,6 +60,39 @@ public abstract class XmlProcessorImplBase implements IXmlProcessor { } } + @Override + public Z read(InputStream inputStream, Class 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 read(Path xml, Class 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 read(String xml, Class 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 { Marshaller marshaller = getContextByObject(obj).createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); @@ -66,6 +107,15 @@ public abstract class XmlProcessorImplBase implements IXmlProcessor { return marshaller; } + private Unmarshaller genUnmarshaller(Class 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 { Class aClass = obj.getClass(); return getContextByClass(aClass); diff --git a/src/main/java/ru/nbch/credit_tracker/service/xml/XmlUtilService.java b/src/main/java/ru/nbch/credit_tracker/service/xml/XmlUtilService.java index 3bdadbc..c80d6cf 100644 --- a/src/main/java/ru/nbch/credit_tracker/service/xml/XmlUtilService.java +++ b/src/main/java/ru/nbch/credit_tracker/service/xml/XmlUtilService.java @@ -2,16 +2,48 @@ package ru.nbch.credit_tracker.service.xml; import jakarta.xml.bind.JAXBException; 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 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 static javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI; + @Service public class XmlUtilService { public IXmlProcessor createJaxbProcessor() throws JAXBException { return new XmlProcessorImplAnyClasses(Charset.forName("windows-1251"), + // unmarshalling classes + MonitoringRequest.class, + // marshalling classes 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)); + } + } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 14d9b0d..8bc9690 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -22,4 +22,7 @@ ct.scoring-data-source-settings.password=tester # TODO Increase file size limit if needed #spring.servlet.multipart.max-file-size=10MB -#spring.servlet.multipart.max-request-size=10MB \ No newline at end of file +#spring.servlet.multipart.max-request-size=10MB + +# xsd paths +ct.xsd-validation.monitoring-request-path=monitoring-request.xsd \ No newline at end of file