xml validation

This commit is contained in:
Michele Artini 2022-12-06 10:16:49 +01:00
parent 781254121d
commit 0f687ae98f
9 changed files with 278 additions and 9 deletions

View File

@ -16,6 +16,7 @@ import org.springframework.stereotype.Service;
import eu.dnetlib.is.resource.model.SimpleResource;
import eu.dnetlib.is.resource.repository.SimpleResourceRepository;
import eu.dnetlib.is.util.InformationServiceException;
import eu.dnetlib.is.util.ResourceValidator;
import eu.dnetlib.is.util.XmlIndenter;
import eu.dnetlib.is.vocabulary.model.Synonym;
import eu.dnetlib.is.vocabulary.model.Vocabulary;
@ -35,6 +36,9 @@ public class OldProfilesImporter {
@Autowired
private VocabularyTermRepository vocabularyTermRepository;
@Autowired
private ResourceValidator resourceValidator;
@Transactional
public String importSimpleResource(final String xml) throws InformationServiceException {
final SAXReader reader = new SAXReader();
@ -92,6 +96,8 @@ public class OldProfilesImporter {
throw new InformationServiceException("Invalid resource type: " + doc.valueOf("//RESOURCE_TYPE/@value"));
}
resourceValidator.validate(res.getType(), resContent.trim());
simpleResourceRepository.save(res);
simpleResourceRepository.setContentById(id, resContent.trim());

View File

@ -17,6 +17,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import eu.dnetlib.is.resource.model.SimpleResource;
import eu.dnetlib.is.resource.repository.SimpleResourceRepository;
import eu.dnetlib.is.util.InformationServiceException;
import eu.dnetlib.is.util.ResourceValidator;
@Service
public class SimpleResourceService {
@ -24,6 +25,9 @@ public class SimpleResourceService {
@Autowired
private SimpleResourceRepository simpleResourceRepository;
@Autowired
private ResourceValidator resourceValidator;
private static final Log log = LogFactory.getLog(SimpleResourceService.class);
public SimpleResource getMetadata(final String id) throws InformationServiceException {
@ -54,7 +58,10 @@ public class SimpleResourceService {
public SimpleResource saveNewResource(final String name,
final String type,
final String description,
final String content) {
final String content) throws InformationServiceException {
resourceValidator.validate(type, content);
final Date now = new Date();
final SimpleResource res = new SimpleResource();
@ -83,11 +90,11 @@ public class SimpleResourceService {
@Transactional
public void saveContent(final String id, final String content) throws InformationServiceException {
if (simpleResourceRepository.existsById(id)) {
simpleResourceRepository.setContentById(id, content);
} else {
throw new InformationServiceException("Resource not found");
}
final SimpleResource res = simpleResourceRepository.findById(id).orElseThrow(() -> new InformationServiceException("Resource not found"));
resourceValidator.validate(res.getType(), content);
simpleResourceRepository.setContentById(id, content);
}
}

View File

@ -0,0 +1,7 @@
package eu.dnetlib.is.util;
public @interface JsonResource {
String value();
}

View File

@ -0,0 +1,107 @@
package eu.dnetlib.is.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.xml.sax.SAXException;
import com.fasterxml.jackson.databind.ObjectMapper;
import eu.dnetlib.is.resource.model.ResourceType;
import eu.dnetlib.is.resource.repository.ResourceTypeRepository;
@Component
public class ResourceValidator {
private final Log log = LogFactory.getLog(ResourceValidator.class);
@Autowired
private ResourceTypeRepository resourceTypeRepository;
public void validate(final String type, final String content) throws InformationServiceException {
final ResourceType rtype = resourceTypeRepository.findById(type).orElseThrow(() -> new InformationServiceException("Invalid type"));
if (rtype.getContentType().equals(MediaType.APPLICATION_XML_VALUE)) {
validateXml(type, content);
} else if (rtype.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
validateJSON(type, content);
}
}
private void validateXml(final String type, final String content) throws InformationServiceException {
final Schema schema = getXmlSchema(type);
if (schema == null) {
log.warn("no registered schema for " + type);
return;
}
final Validator validator = schema.newValidator();
try {
validator.validate(new StreamSource(new StringReader(content)));
} catch (final Exception e) {
throw new InformationServiceException("invalid resource", e);
}
}
private Schema getXmlSchema(final String type) {
try {
final InputStream is = getClass().getResourceAsStream("/schemas/" + type + ".xsd");
if (is == null) { return null; }
final String schemaSource = IOUtils.toString(is, StandardCharsets.UTF_8.name());
if (StringUtils.isBlank(schemaSource)) { return null; }
return SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(new StreamSource(new StringReader(schemaSource)));
} catch (final SAXException | IOException e) {
log.fatal("cannot parse resource type schema", e);
return null;
}
}
private void validateJSON(final String type, final String content) throws InformationServiceException {
final Class<?> clazz = getJsonClass(type);
if (clazz == null) {
log.warn("no registered Class for " + type);
}
final ObjectMapper mapper = new ObjectMapper();
try {
mapper.readValue(content, clazz);
} catch (final Exception e) {
throw new InformationServiceException("invalid resource", e);
}
}
private Class<?> getJsonClass(final String type) {
final ClassLoader classLoader = ResourceValidator.class.getClassLoader();
final ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true);
provider.addIncludeFilter(new AnnotationTypeFilter(JsonResource.class));
provider.setResourceLoader(new PathMatchingResourcePatternResolver(classLoader));
for (final BeanDefinition bd : provider.findCandidateComponents("eu.dnetlib")) {
if (bd.getClass().getAnnotation(JsonResource.class).value().equals(type)) { return bd.getClass(); }
}
return null;
}
}

View File

@ -29,7 +29,7 @@ public class XmlIndenter {
public static String indent(final Node node) {
try {
final StringWriter sw = new StringWriter();
final XMLWriter writer = new XMLWriter(sw, new OutputFormat("\t", true));
final XMLWriter writer = new XMLWriter(sw, OutputFormat.createPrettyPrint());
writer.write(node);
writer.close();
return sw.toString();

View File

@ -0,0 +1,18 @@
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
<xs:element type="CLEANERRULESType" name="CLEANER_RULES" />
<xs:complexType name="CLEANERRULESType">
<xs:sequence>
<xs:element name="RULE" maxOccurs="unbounded" type="RULEType" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="RULEType">
<xs:attribute type="xs:string" name="xpath" use="required" />
<xs:attribute type="xs:string" name="vocabularies" use="optional" />
<xs:attribute type="xs:string" name="groovy" use="optional" />
<xs:attribute type="xs:boolean" name="strict" use="optional" />
</xs:complexType>
</xs:schema>

View File

@ -0,0 +1,122 @@
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
<xs:element name="HADOOP_JOB">
<xs:complexType>
<xs:sequence>
<xs:element ref="DESCRIPTION"/>
<xs:element ref="STATIC_CONFIGURATION"/>
<xs:element ref="JOB_INTERFACE"/>
<xs:element ref="SCAN" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute name="name" use="required" type="xs:string"/>
<xs:attribute name="type" use="required">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="mapreduce"/>
<xs:enumeration value="oozie"/>
<xs:enumeration value="admin"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:element>
<xs:element name="DESCRIPTION" type="xs:string"/>
<xs:element name="STATIC_CONFIGURATION">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded" ref="PROPERTY"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="PROPERTY">
<xs:complexType>
<xs:attribute name="key" use="required" type="xs:NCName"/>
<xs:attribute name="value" use="required"/>
</xs:complexType>
</xs:element>
<xs:element name="JOB_INTERFACE">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded" minOccurs="0" ref="PARAM"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="PARAM">
<xs:complexType>
<xs:attribute name="description" use="required"/>
<xs:attribute name="name" use="required" type="xs:NCName"/>
<xs:attribute name="required" use="required" type="xs:boolean"/>
</xs:complexType>
</xs:element>
<xs:element name="SCAN">
<xs:complexType>
<xs:sequence>
<xs:element ref="FILTERS"/>
<xs:element ref="FAMILIES"/>
</xs:sequence>
<xs:attribute name="caching" use="optional" type="xs:integer"/>
</xs:complexType>
</xs:element>
<xs:element name="FILTERS">
<xs:complexType>
<xs:sequence>
<xs:element ref="FILTER" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence><!-- The operator is OPTIONAL, when it is missing the default value is MUST_PASS_ALL -->
<xs:attribute name="operator" use="optional" type="operatortype"/>
</xs:complexType>
</xs:element>
<xs:element name="FILTER">
<xs:complexType>
<xs:attribute name="type" use="required" type="filtertype"/>
<xs:attribute name="param" type="xs:string" use="optional"/>
<xs:attribute name="value" type="xs:string" use="optional"/>
</xs:complexType>
</xs:element>
<xs:element name="FAMILIES">
<xs:complexType>
<xs:sequence>
<xs:element ref="FAMILY" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="FAMILY">
<xs:complexType>
<xs:attribute name="param" use="optional" type="xs:string"/>
<xs:attribute name="value" use="optional" type="xs:string"/>
</xs:complexType>
</xs:element>
<xs:element name="STATUS">
<xs:complexType>
<xs:sequence>
<xs:element ref="LAST_SUBMISSION_DATE"/>
<xs:element ref="RUNNING_INSTANCES"/>
<xs:element ref="CUMULATIVE_RUN"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="LAST_SUBMISSION_DATE">
<xs:complexType>
<xs:attribute name="value" use="required" type="xs:dateTime"/>
</xs:complexType>
</xs:element>
<xs:element name="RUNNING_INSTANCES">
<xs:complexType>
<xs:attribute name="value" use="required" type="xs:integer"/>
</xs:complexType>
</xs:element>
<xs:element name="CUMULATIVE_RUN">
<xs:complexType>
<xs:attribute name="value" use="required" type="xs:integer"/>
</xs:complexType>
</xs:element>
<xs:simpleType name="filtertype">
<xs:restriction base="xs:string">
<xs:enumeration value="prefix"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="operatortype">
<xs:restriction base="xs:string">
<xs:enumeration value="MUST_PASS_ALL"/>
<xs:enumeration value="MUST_PASS_ONE"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>

View File

@ -49,6 +49,7 @@ app.controller('resourcesController', function($scope, $http) {
'content' : r.content
})).then(function successCallback(res) {
alert("Resource saved");
$('#newResourceModal').modal('hide');
$scope.reload();
}, function errorCallback(res) {
alert('ERROR: ' + res.data.message);
@ -71,6 +72,7 @@ app.controller('resourcesController', function($scope, $http) {
'content' : content
})).then(function successCallback(res) {
alert("Resource saved");
$('#editContentModal').modal('hide');
}, function errorCallback(res) {
alert('ERROR: ' + res.data.message);
});

View File

@ -108,7 +108,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-sm btn-primary" data-dismiss="modal" ng-click="createNewResource(tmpRes)" ng-disabled="!tmpRes.name || !tmpRes.content">Submit</button>
<button type="submit" class="btn btn-sm btn-primary" ng-click="createNewResource(tmpRes)" ng-disabled="!tmpRes.name || !tmpRes.content">Submit</button>
</div>
</form>
</div>
@ -131,7 +131,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-sm btn-primary" data-dismiss="modal" ng-click="saveContent(tmpRes.id, tmpContent)" ng-disabled="!tmpContent">Submit</button>
<button type="submit" class="btn btn-sm btn-primary" ng-click="saveContent(tmpRes.id, tmpContent)" ng-disabled="!tmpContent">Submit</button>
</div>
</form>
</div>