/*
* 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.prism.schema;
import com.evolveum.midpoint.prism.XmlEntityResolver;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import org.w3c.dom.ls.LSInput;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
/**
* @author semancik
* @author mederly
*
* TODO refactor this a bit more
*/
public class XmlEntityResolverImpl implements XmlEntityResolver {
private static final Trace LOGGER = TraceManager.getTrace(XmlEntityResolverImpl.class);
private final SchemaRegistryImpl schemaRegistry;
public XmlEntityResolverImpl(SchemaRegistryImpl schemaRegistry) {
this.schemaRegistry = schemaRegistry;
}
/*
* Although not sure when resolveEntity and resolveResource is called, general schema is the following:
* 1. For schemas that are imported via xsd:import, we use schemaLocation = namespaceURI, with some exceptions
* (we of course don't modify xsd:imports in standard schemas + for some historic reasons there is a difference
* for 'enc' and 'dsig' schemas: namespaceURI ends with '#', whereas schemaLocation does not)
* 2. For schemas that are included via xsd:include (currently: fragments of common-3 schema), we use
* namespaceURI of the owning schema (e.g. .../common-3), whereas schemaLocation is URI derived from the
* namespace by including the fragment name (e.g. .../common-notifications-3).
*
* XSD parsers (the ones used by xjc and runtime parsing) seem to do the following:
* 1. When encountering xsd:import, they look by publicId = namespaceURI, systemId = schemaLocation OR
* sometimes with publicId = null, systemId = schemaLocation (why?)
* 2. When encountering xsd:include, they look by publicId = null, systemId = schemaLocation
* 3. When encountering XML entity declaration that specifies publicId and systemId, look by them.
*
* See the respective methods.
*
*/
/* (non-Javadoc)
* @see org.xml.sax.EntityResolver#resolveEntity(java.lang.String, java.lang.String)
*/
@Override
public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
LOGGER.trace("--- Resolving entity with publicID: {}, systemID: {}", publicId, systemId);
InputSource inputSource = resolveResourceFromRegisteredSchemas(publicId, systemId);
if (inputSource == null) {
inputSource = resolveResourceUsingBuiltinResolver(null, null, publicId, systemId, null);
}
if (inputSource == null) {
LOGGER.error("Unable to resolve entity with publicID: {}, systemID: {}",new Object[]{publicId, systemId});
return null;
}
LOGGER.trace("==> Resolved entity with publicID: {}, systemID: {} : {}", publicId, systemId, inputSource);
return inputSource;
}
/* (non-Javadoc)
* @see org.w3c.dom.ls.LSResourceResolver#resolveResource(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String)
*/
@Override
public LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId,
String baseURI) {
LOGGER.trace("--- Resolving resource of type {}, namespaceURI: {}, publicID: {}, systemID: {}, base URI: {}", type, namespaceURI, publicId, systemId, baseURI);
InputSource inputSource = resolveResourceFromRegisteredSchemas(publicId, systemId);
if (inputSource == null) {
inputSource = resolveResourceUsingBuiltinResolver(type, namespaceURI, publicId, systemId, baseURI);
}
if (inputSource == null) {
LOGGER.error("Unable to resolve resource of type {}, namespaceURI: {}, publicID: {}, systemID: {}, baseURI: {}",new Object[]{type, namespaceURI, publicId, systemId, baseURI});
return null;
}
LOGGER.trace("==> Resolved resource of type {}, namespaceURI: {}, publicID: {}, systemID: {}, baseURI: {} : {}",new Object[]{type, namespaceURI, publicId, systemId, baseURI, inputSource});
return new Input(publicId, systemId, inputSource.getByteStream());
}
// schema fragments (e.g. common-model-context-3) will be obviously not found by this method
private InputSource resolveResourceFromRegisteredSchemas(String publicId, String systemId) {
InputSource source = resolveResourceFromRegisteredSchemasByNamespace(publicId);
if (source == null) {
// give a chance to systemId - in cases of xsd:import namespaceURI=<ns>, schemaLocation=<ns> that
// (for some weird reason) result in search with publicId=null, systemId=<ns>
source = resolveResourceFromRegisteredSchemasByNamespace(systemId);
}
LOGGER.trace("...... Result of registered schema resolve for publicId: {}, systemId: {}: {}", publicId, systemId, source);
return source;
}
private InputSource resolveResourceFromRegisteredSchemasByNamespace(String namespaceURI) {
if (namespaceURI != null) {
if (schemaRegistry.getParsedSchemas().containsKey(namespaceURI)) {
SchemaDescription schemaDescription = schemaRegistry.getParsedSchemas().get(namespaceURI);
if (schemaDescription.canInputStream()) {
InputStream inputStream = schemaDescription.openInputStream();
InputSource source = new InputSource();
source.setByteStream(inputStream);
//source.setSystemId(schemaDescription.getPath());
// Make sure that both publicId and systemId are always set to schema namespace
// this helps to avoid double processing of the schemas
source.setSystemId(namespaceURI);
source.setPublicId(namespaceURI);
return source;
} else {
throw new IllegalStateException("Requested resolution of schema "+schemaDescription.getSourceDescription()+" that does not support input stream");
}
}
}
return null;
}
public InputSource resolveResourceUsingBuiltinResolver(String type, String namespaceURI, String publicId, String systemId,
String baseURI) {
InputSource inputSource = null;
try {
// we first try to use traditional pair of publicId + systemId
// the use of namespaceUri can be misleading in case of schema fragments:
// e.g. when xsd:including common-model-context-3 the publicId=null, systemId=.../common-model-context-3 but nsUri=.../common-3
if (inputSource == null) {
inputSource = schemaRegistry.getBuiltinSchemaResolver().resolveEntity(publicId, systemId);
LOGGER.trace("...... Result of using builtin resolver by publicId + systemId: {}", inputSource);
}
// in some weird cases (e.g. when publicId=null, systemId=xml.xsd) we go with namespaceUri (e.g. http://www.w3.org/XML/1998/namespace)
// it's a kind of unfortunate magic here
if (inputSource == null && namespaceURI != null) {
inputSource = schemaRegistry.getBuiltinSchemaResolver().resolveEntity(namespaceURI, systemId);
LOGGER.trace("...... Result of using builtin resolver by namespaceURI + systemId: {}", inputSource);
}
} catch (SAXException e) {
LOGGER.error("XML parser error resolving reference of type {}, namespaceURI: {}, publicID: {}, systemID: {}, baseURI: {}: {}",new Object[]{type, namespaceURI, publicId, systemId, baseURI, e.getMessage(), e});
// TODO: better error handling
return null;
} catch (IOException e) {
LOGGER.error("IO error resolving reference of type {}, namespaceURI: {}, publicID: {}, systemID: {}, baseURI: {}: {}",new Object[]{type, namespaceURI, publicId, systemId, baseURI, e.getMessage(), e});
// TODO: better error handling
return null;
}
return inputSource;
}
class Input implements LSInput {
private String publicId;
private String systemId;
private final BufferedInputStream inputStream;
public String getPublicId() {
return publicId;
}
public void setPublicId(String publicId) {
this.publicId = publicId;
}
public String getBaseURI() {
return null;
}
public InputStream getByteStream() {
return null;
}
public boolean getCertifiedText() {
return false;
}
public Reader getCharacterStream() {
return null;
}
public String getEncoding() {
return null;
}
public String getStringData() {
synchronized (inputStream) {
try {
byte[] input = new byte[inputStream.available()];
//noinspection ResultOfMethodCallIgnored
inputStream.read(input);
return new String(input);
} catch (IOException e) {
LOGGER.error("IO error creating LSInput for publicID: {}, systemID: {}: {}", publicId, systemId, e.getMessage(), e);
// TODO: better error handling
return null;
}
}
}
public void setBaseURI(String baseURI) {
}
public void setByteStream(InputStream byteStream) {
}
public void setCertifiedText(boolean certifiedText) {
}
public void setCharacterStream(Reader characterStream) {
}
public void setEncoding(String encoding) {
}
public void setStringData(String stringData) {
}
public String getSystemId() {
return systemId;
}
public void setSystemId(String systemId) {
this.systemId = systemId;
}
public BufferedInputStream getInputStream() {
return inputStream;
}
public Input(String publicId, String sysId, InputStream input) {
this.publicId = publicId;
this.systemId = sysId;
this.inputStream = new BufferedInputStream(input);
}
}
//endregion
}