/*
* Copyright (c) 2010-2016 Evolveum
*
* 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.
*/
package com.evolveum.midpoint.common.validator;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.validation.Schema;
import com.evolveum.midpoint.util.QNameUtil;
import org.apache.commons.lang.StringUtils;
import org.codehaus.staxmate.dom.DOMConverter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import com.evolveum.midpoint.prism.Objectable;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.PrismObject;
import com.evolveum.midpoint.prism.schema.SchemaRegistry;
import com.evolveum.midpoint.schema.constants.SchemaConstants;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.schema.util.ResourceTypeUtil;
import com.evolveum.midpoint.util.DOMUtil;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.util.exception.SystemException;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ResourceType;
import com.evolveum.prism.xml.ns._public.types_3.PolyStringType;
/**
*
*
* @author Radovan Semancik
*
*/
public class Validator {
private static final Trace LOGGER = TraceManager.getTrace(Validator.class);
private static final String INPUT_STREAM_CHARSET = "utf-8";
private static final String OPERATION_PREFIX = Validator.class.getName() + ".";
private static final String OPERATION_RESOURCE_NAMESPACE_CHECK = OPERATION_PREFIX
+ "resourceNamespaceCheck";
private static final String OPERATION_RESOURCE_BASICS_CHECK = OPERATION_PREFIX + "objectBasicsCheck";
private static final String START_LINE_NUMBER = "startLineNumber";
private static final String END_LINE_NUMBER = "endLineNumber";
private boolean verbose = false;
private boolean validateSchemas = true;
private boolean allowAnyType = false;
private EventHandler handler;
private DOMConverter domConverter = new DOMConverter();
private Unmarshaller unmarshaller = null;
private PrismContext prismContext;
private Schema midPointJavaxSchema;
private javax.xml.validation.Validator xsdValidator;
long progress = 0;
long errors = 0;
long stopAfterErrors = 0;
public Validator(PrismContext prismContext) {
this.prismContext = prismContext;
this.handler = null;
initialize();
}
public Validator(PrismContext prismContext, EventHandler handler) {
this.prismContext = prismContext;
this.handler = handler;
initialize();
}
private void initialize() {
if (prismContext == null) {
throw new IllegalStateException("No prism context set during validator initialization");
}
SchemaRegistry schemaRegistry = prismContext.getSchemaRegistry();
midPointJavaxSchema = schemaRegistry.getJavaxSchema();
xsdValidator = midPointJavaxSchema.newValidator();
xsdValidator.setResourceResolver(prismContext.getEntityResolver());
}
public EventHandler getHandler() {
return handler;
}
public void setHandler(EventHandler handler) {
this.handler = handler;
}
public PrismContext getPrismContext() {
return prismContext;
}
public boolean getVerbose() {
return verbose;
}
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
public void setValidateSchema(boolean validateSchemas) {
this.validateSchemas = validateSchemas;
}
public boolean getValidateSchema() {
return validateSchemas;
}
public void setAllowAnyType(boolean allowAnyType) {
this.allowAnyType = allowAnyType;
}
public boolean getAllowAnyType() {
return allowAnyType;
}
public long getStopAfterErrors() {
return stopAfterErrors;
}
public void setStopAfterErrors(long stopAfterErrors) {
this.stopAfterErrors = stopAfterErrors;
}
public long getProgress() {
return progress;
}
public long getErrors() {
return errors;
}
public void validate(InputStream inputStream, OperationResult validatorResult,
String objectResultOperationName) {
XMLStreamReader stream = null;
try {
Map<String, String> rootNamespaceDeclarations = new HashMap<String, String>();
XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
stream = xmlInputFactory.createXMLStreamReader(inputStream);
int eventType = stream.nextTag();
if (eventType == XMLStreamConstants.START_ELEMENT) {
if (!QNameUtil.match(stream.getName(), SchemaConstants.C_OBJECTS)) {
// This has to be an import file with a single objects. Try
// to process it.
OperationResult objectResult = validatorResult.createSubresult(objectResultOperationName);
progress++;
objectResult.addContext(OperationResult.CONTEXT_PROGRESS, progress);
EventResult cont = null;
try {
cont = readFromStreamAndValidate(stream, objectResult,
rootNamespaceDeclarations, validatorResult);
} catch (RuntimeException e) {
// Make sure that unexpected error is recorded.
objectResult.recordFatalError(e);
throw e;
}
if (!cont.isCont()) {
String message = null;
if (cont.getReason() != null) {
message = cont.getReason();
} else {
message = "Object validation failed (no reason given)";
}
if (objectResult.isUnknown()) {
objectResult.recordFatalError(message);
}
validatorResult.recordFatalError(message);
return;
}
// return to avoid processing objects in loop
validatorResult.computeStatus("Validation failed", "Validation warnings");
return;
}
// Extract root namespace declarations
for (int i = 0; i < stream.getNamespaceCount(); i++) {
rootNamespaceDeclarations.put(stream.getNamespacePrefix(i), stream.getNamespaceURI(i));
}
} else {
throw new SystemException("StAX Malfunction?");
}
while (stream.hasNext()) {
eventType = stream.next();
if (eventType == XMLStreamConstants.START_ELEMENT) {
OperationResult objectResult = validatorResult.createSubresult(objectResultOperationName);
progress++;
objectResult.addContext(OperationResult.CONTEXT_PROGRESS, progress);
EventResult cont = null;
try {
// Read and validate individual object from the stream
cont = readFromStreamAndValidate(stream, objectResult,
rootNamespaceDeclarations, validatorResult);
} catch (RuntimeException e) {
if (objectResult.isUnknown()) {
// Make sure that unexpected error is recorded.
objectResult.recordFatalError(e);
}
throw e;
}
if (objectResult.isError()) {
errors++;
}
objectResult.cleanupResult();
validatorResult.summarize();
if (cont.isStop()) {
if (cont.getReason() != null) {
validatorResult.recordFatalError("Processing has been stopped: "
+ cont.getReason());
} else {
validatorResult.recordFatalError("Processing has been stopped");
}
// This means total stop, no other objects will be
// processed
return;
}
if (!cont.isCont()) {
if (stopAfterErrors > 0 && errors >= stopAfterErrors) {
validatorResult.recordFatalError("Too many errors (" + errors + ")");
return;
}
}
}
}
} catch (XMLStreamException ex) {
// validatorResult.recordFatalError("XML parsing error: " +
// ex.getMessage()+" on line "+stream.getLocation().getLineNumber(),ex);
validatorResult.recordFatalError("XML parsing error: " + ex.getMessage(), ex);
if (handler != null) {
handler.handleGlobalError(validatorResult);
}
return;
}
// Error count is sufficient. Detailed messages are in subresults
validatorResult.computeStatus(errors + " errors, " + (progress - errors) + " passed");
}
private EventResult readFromStreamAndValidate(XMLStreamReader stream, OperationResult objectResult,
Map<String, String> rootNamespaceDeclarations, OperationResult validatorResult) {
objectResult.addContext(START_LINE_NUMBER, stream.getLocation().getLineNumber());
Document objectDoc;
try {
// Parse the object from stream to DOM
objectDoc = domConverter.buildDocument(stream);
} catch (XMLStreamException ex) {
validatorResult.recordFatalError("XML parsing error: " + ex.getMessage(), ex);
if (handler != null) {
handler.handleGlobalError(validatorResult);
}
objectResult.recordFatalError(ex);
return EventResult.skipObject(ex.getMessage());
}
objectResult.addContext(END_LINE_NUMBER, stream.getLocation().getLineNumber());
// This element may not have complete namespace definitions for a
// stand-alone
// processing, therefore copy namespace definitions from the root
// element
Element objectElement = DOMUtil.getFirstChildElement(objectDoc);
DOMUtil.setNamespaceDeclarations(objectElement, rootNamespaceDeclarations);
return validateObjectInternal(objectElement, objectResult, validatorResult);
}
public EventResult validateObject(String stringXml, OperationResult objectResult) {
Document objectDoc = DOMUtil.parseDocument(stringXml);
Element objectElement = DOMUtil.getFirstChildElement(objectDoc);
return validateObjectInternal(objectElement, objectResult, objectResult);
}
public EventResult validateObject(Element objectElement, OperationResult objectResult) {
return validateObjectInternal(objectElement, objectResult, objectResult);
}
private EventResult validateObjectInternal(Element objectElement, OperationResult objectResult, OperationResult validatorResult) {
try {
Node postValidationTree = null;
if (validateSchemas) {
postValidationTree = validateSchema(objectElement, objectResult);
if (postValidationTree == null) {
// There was an error
return EventResult.skipObject(objectResult.getMessage());
}
}
if (handler != null) {
EventResult cont;
try {
cont = handler.preMarshall(objectElement, postValidationTree, objectResult);
} catch (RuntimeException e) {
objectResult.recordFatalError("Internal error: preMarshall call failed: "+e.getMessage(), e);
throw e;
}
if (!cont.isCont()) {
if (objectResult.isUnknown()) {
objectResult.recordFatalError("Stopped after preMarshall, no reason given");
}
return cont;
}
}
if (!objectResult.isAcceptable()) {
// Schema validation or preMarshall has failed. No point to
// continue with this object.
if (objectResult.isUnknown()) {
objectResult.recordFatalError("Result not acceptable after preMarshall, no reason given");
}
return EventResult.skipObject();
}
PrismObject<? extends Objectable> object = prismContext.parserFor(objectElement).parse();
try {
object.checkConsistence();
} catch (RuntimeException e) {
objectResult.recordFatalError("Internal object inconsistence, probably a parser bug: "+e.getMessage(), e);
return EventResult.skipObject(e.getMessage());
}
Objectable objectType = null;
if (object != null) {
objectType = object.asObjectable();
}
if (verbose) {
LOGGER.trace("Processing OID " + objectType.getOid());
}
objectResult.addContext(OperationResult.CONTEXT_OBJECT, objectType);
validateObject(objectType, objectResult);
if (handler != null) {
EventResult cont;
try {
cont = handler.postMarshall(object, objectElement, objectResult);
} catch (RuntimeException e) {
// Make sure that unhandled exceptions are recorded in object result before they are rethrown
objectResult.recordFatalError("Internal error: postMarshall call failed: "+e.getMessage(), e);
throw e;
}
if (!cont.isCont()) {
if (objectResult.isUnknown()) {
objectResult.recordFatalError("Stopped after postMarshall, no reason given");
}
return cont;
}
}
objectResult.recomputeStatus();
return EventResult.cont();
} catch (SchemaException ex) {
if (verbose) {
ex.printStackTrace();
}
if (handler != null) {
try {
handler.handleGlobalError(validatorResult);
} catch (RuntimeException e) {
// Make sure that unhandled exceptions are recorded in object result before they are rethrown
objectResult.recordFatalError("Internal error: handleGlobalError call failed: "+e.getMessage(), e);
throw e;
}
}
objectResult.recordFatalError(ex);
return EventResult.skipObject(ex.getMessage());
} catch (RuntimeException ex) {
validatorResult.recordFatalError("Couldn't parse object: " + ex.getMessage(), ex);
if (verbose) {
ex.printStackTrace();
}
if (handler != null) {
try {
handler.handleGlobalError(validatorResult);
} catch (RuntimeException e) {
// Make sure that unhandled exceptions are recorded in object result before they are rethrown
objectResult.recordFatalError("Internal error: handleGlobalError call failed: "+e.getMessage(), e);
throw e;
}
}
objectResult.recordFatalError(ex);
return EventResult.skipObject(ex.getMessage());
}
}
// this was made public to allow validation of pre-parsed non-prism documents
public Node validateSchema(Element objectDoc, OperationResult objectResult) {
OperationResult result = objectResult.createSubresult(Validator.class.getName() + ".validateSchema");
DOMResult validationResult = new DOMResult();
try {
xsdValidator.validate(new DOMSource(objectDoc), validationResult);
} catch (SAXException e) {
result.recordFatalError("Validation error: " + e.getMessage(), e);
objectResult.computeStatus("Validation error: " + e.getMessage());
return null;
} catch (IOException e) {
result.recordFatalError("IO error during validation: " + e.getMessage(), e);
objectResult.computeStatus("IO error during validation: " + e.getMessage());
return null;
}
result.recordSuccess();
return validationResult.getNode();
}
public void validateObject(Objectable object, OperationResult objectResult) {
// Check generic object properties
checkBasics(object, objectResult);
// Type-specific checks
if (object instanceof ResourceType) {
ResourceType resource = (ResourceType) object;
checkResource(resource, objectResult);
}
// TODO: more checks
objectResult.recomputeStatus("Object validation has failed", "Validation warning");
objectResult.recordSuccessIfUnknown();
}
// BIG checks - checks that create subresults
void checkBasics(Objectable object, OperationResult objectResult) {
OperationResult subresult = objectResult.createSubresult(OPERATION_RESOURCE_BASICS_CHECK);
checkName(object, object.getName(), "name", subresult);
subresult.recordSuccessIfUnknown();
}
void checkResource(ResourceType resource, OperationResult objectResult) {
OperationResult subresult = objectResult.createSubresult(OPERATION_RESOURCE_NAMESPACE_CHECK);
checkUri(resource, ResourceTypeUtil.getResourceNamespace(resource), "namespace", subresult);
subresult.recordSuccessIfUnknown();
}
// Small checks - checks that don't create subresults
void checkName(Objectable object, PolyStringType value, String propertyName, OperationResult subResult) {
// TODO: check for all whitespaces
// TODO: check for bad characters
if (value == null) {
error("Null property", object, propertyName, subResult);
return;
}
String orig = value.getOrig();
if (orig == null || orig.isEmpty()) {
error("Empty property", object, propertyName, subResult);
}
}
void checkUri(Objectable object, String value, String propertyName, OperationResult subResult) {
// TODO: check for all whitespaces
// TODO: check for bad characters
if (StringUtils.isEmpty(value)) {
error("Empty property", object, propertyName, subResult);
return;
}
try {
URI uri = new URI(value);
if (uri.getScheme() == null) {
error("URI is supposed to be absolute", object, propertyName, subResult);
}
} catch (URISyntaxException ex) {
error("Wrong URI syntax: " + ex, object, propertyName, subResult);
}
}
void error(String message, Objectable object, OperationResult subResult) {
subResult.addContext(OperationResult.CONTEXT_OBJECT, object);
subResult.recordFatalError(message);
}
void error(String message, Objectable object, String propertyName, OperationResult subResult) {
subResult.addContext(OperationResult.CONTEXT_OBJECT, object);
subResult.addContext(OperationResult.CONTEXT_ITEM, propertyName);
subResult.recordFatalError("<" + propertyName + ">: " + message);
}
}