web-dev-qa-db-ja.com

Javaを使用してXMLファイルを検証し、XSDにインクルードを含める方法は?

Java 5 javax.xml.validation.Validatorを使用してXMLファイルを検証しています。インポートのみを使用する1つのスキーマに対してそれを実行し、すべてが正常に機能します。今、インポートと1つのインクルードを使用する別のスキーマで検証します。私が抱えている問題は、メインスキーマの要素が無視されることです。検証では、宣言が見つからないことが示されています。

ここに私がスキーマを構築する方法があります:

InputStream includeInputStream = getClass().getClassLoader().getResource("include.xsd").openStream();
InputStream importInputStream = getClass().getClassLoader().getResource("import.xsd").openStream();
InputStream mainInputStream = getClass().getClassLoader().getResource("main.xsd").openStream();
Source[] sourceSchema = new SAXSource[]{includeInputStream , importInputStream, 
mainInputStream };
Schema schema = factory.newSchema(sourceSchema);

これがmain.xsdの宣言の抜粋です

<xsd:schema xmlns="http://schema.omg.org/spec/BPMN/2.0" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:import="http://www.foo.com/import" targetNamespace="http://main/namespace" elementFormDefault="qualified" attributeFormDefault="unqualified">
    <xsd:import namespace="http://www.foo.com/import" schemaLocation="import.xsd"/>
    <xsd:include schemaLocation="include.xsd"/>
    <xsd:element name="element" type="tElement"/>
    <...>
</xsd:schema>

含まれているXSDのコードをmain.xsdにコピーすると、問題なく動作します。そうしないと、検証で「要素」の宣言が見つかりません。

29
Melanie

これを機能させるには LSResourceResolver を使用する必要があります。以下のサンプルコードをご覧ください。

検証メソッド:

// note that if your XML already declares the XSD to which it has to conform, then there's no need to declare the schemaName here
void validate(String xml, String schemaName) throws Exception {

    DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
    builderFactory.setNamespaceAware(true);

    DocumentBuilder parser = builderFactory
            .newDocumentBuilder();

    // parse the XML into a document object
    Document document = parser.parse(new StringInputStream(xml));

    SchemaFactory factory = SchemaFactory
            .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);

    // associate the schema factory with the resource resolver, which is responsible for resolving the imported XSD's
    factory.setResourceResolver(new ResourceResolver());

            // note that if your XML already declares the XSD to which it has to conform, then there's no need to create a validator from a Schema object
    Source schemaFile = new StreamSource(getClass().getClassLoader()
            .getResourceAsStream(schemaName));
    Schema schema = factory.newSchema(schemaFile);

    Validator validator = schema.newValidator();
    validator.validate(new DOMSource(document));
}

リソースリゾルバの実装:

public class ResourceResolver  implements LSResourceResolver {

public LSInput resolveResource(String type, String namespaceURI,
        String publicId, String systemId, String baseURI) {

     // note: in this sample, the XSD's are expected to be in the root of the classpath
    InputStream resourceAsStream = this.getClass().getClassLoader()
            .getResourceAsStream(systemId);
    return new Input(publicId, systemId, resourceAsStream);
}

 }

リソースリゾルバーによって返される入力実装:

public class Input implements LSInput {

private String publicId;

private String systemId;

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()];
            inputStream.read(input);
            String contents = new String(input);
            return contents;
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("Exception " + e);
            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 void setInputStream(BufferedInputStream inputStream) {
    this.inputStream = inputStream;
}

private BufferedInputStream inputStream;

public Input(String publicId, String sysId, InputStream input) {
    this.publicId = publicId;
    this.systemId = sysId;
    this.inputStream = new BufferedInputStream(input);
}
}
59
Stefan De Boey

受け入れられた回答は完全に問題ありませんが、Java 8を変更しないと機能しません。インポートされたスキーマが読み込まれるベースパスを指定できると便利です。

Java 8で、ルートパス以外の埋め込みスキーマパスを指定できるようにする次のコードを使用しました。

import com.Sun.org.Apache.xerces.internal.dom.DOMInputImpl;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSResourceResolver;

import Java.io.InputStream;
import Java.util.Objects;

public class ResourceResolver implements LSResourceResolver {

    private String basePath;

    public ResourceResolver(String basePath) {
        this.basePath = basePath;
    }

    @Override
    public LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId, String baseURI) {
        // note: in this sample, the XSD's are expected to be in the root of the classpath
        InputStream resourceAsStream = this.getClass().getClassLoader()
                .getResourceAsStream(buildPath(systemId));
        Objects.requireNonNull(resourceAsStream, String.format("Could not find the specified xsd file: %s", systemId));
        return new DOMInputImpl(publicId, systemId, baseURI, resourceAsStream, "UTF-8");
    }

    private String buildPath(String systemId) {
        return basePath == null ? systemId : String.format("%s/%s", basePath, systemId);
    }
}

この実装は、スキーマを読み取れない場合に意味のあるメッセージをユーザーに提供します。

5
gil.fernandes

AMegmondoEmberによって この投稿 にいくつかの変更を加える必要がありました

私のメインスキーマファイルには兄弟フォルダーからのいくつかのインクルードがあり、インクルードファイルにはローカルフォルダーからのいくつかのインクルードも含まれていました。また、現在のリソースのベースリソースパスと相対パスを追跡する必要がありました。このコードは私にとってはうまくいきますが、すべてのxsdファイルが一意の名前を持っていると想定していることに注意してください。同じ名前のxsdファイルがいくつかあるが、異なるパスに異なるコンテンツがある場合、おそらく問題が発生します。

import Java.io.ByteArrayInputStream;
import Java.io.InputStream;
import Java.util.HashMap;
import Java.util.Map;
import Java.util.Scanner;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSResourceResolver;

/**
 * The Class ResourceResolver.
 */
public class ResourceResolver implements LSResourceResolver {

    /** The logger. */
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /** The schema base path. */
    private final String schemaBasePath;

    /** The path map. */
    private Map<String, String> pathMap = new HashMap<String, String>();

    /**
     * Instantiates a new resource resolver.
     *
     * @param schemaBasePath the schema base path
     */
    public ResourceResolver(String schemaBasePath) {
        this.schemaBasePath = schemaBasePath;
        logger.warn("This LSResourceResolver implementation assumes that all XSD files have a unique name. "
                + "If you have some XSD files with same name but different content (at different paths) in your schema structure, "
                + "this resolver will fail to include the other XSD files except the first one found.");
    }

    /* (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) {
        // The base resource that includes this current resource
        String baseResourceName = null;
        String baseResourcePath = null;
        // Extract the current resource name
        String currentResourceName = systemId.substring(systemId
                .lastIndexOf("/") + 1);

        // If this resource hasn't been added yet
        if (!pathMap.containsKey(currentResourceName)) {
            if (baseURI != null) {
                baseResourceName = baseURI
                        .substring(baseURI.lastIndexOf("/") + 1);
            }

            // we dont need "./" since getResourceAsStream cannot understand it
            if (systemId.startsWith("./")) {
                systemId = systemId.substring(2, systemId.length());
            }

            // If the baseResourcePath has already been discovered, get that
            // from pathMap
            if (pathMap.containsKey(baseResourceName)) {
                baseResourcePath = pathMap.get(baseResourceName);
            } else {
                // The baseResourcePath should be the schemaBasePath
                baseResourcePath = schemaBasePath;
            }

            // Read the resource as input stream
            String normalizedPath = getNormalizedPath(baseResourcePath, systemId);
            InputStream resourceAsStream = this.getClass().getClassLoader()
                    .getResourceAsStream(normalizedPath);

            // if the current resource is not in the same path with base
            // resource, add current resource's path to pathMap
            if (systemId.contains("/")) {
                pathMap.put(currentResourceName, normalizedPath.substring(0,normalizedPath.lastIndexOf("/")+1));
            } else {
                // The current resource should be at the same path as the base
                // resource
                pathMap.put(systemId, baseResourcePath);
            }
            Scanner s = new Scanner(resourceAsStream).useDelimiter("\\A");
            String s1 = s.next().replaceAll("\\n", " ") // the parser cannot understand elements broken down multiple lines e.g. (<xs:element \n name="buxing">)
                    .replace("\\t", " ") // these two about whitespaces is only for decoration
                    .replaceAll("\\s+", " ").replaceAll("[^\\x20-\\x7e]", ""); // some files has a special character as a first character indicating utf-8 file
            InputStream is = new ByteArrayInputStream(s1.getBytes());

            return new LSInputImpl(publicId, systemId, is); // same as Input class
        }

        // If this resource has already been added, do not add the same resource again. It throws
        // "org.xml.sax.SAXParseException: sch-props-correct.2: A schema cannot contain two global components with the same name; this schema contains two occurrences of ..."
        // return null instead.
        return null;
    }

    /**
     * Gets the normalized path.
     *
     * @param basePath the base path
     * @param relativePath the relative path
     * @return the normalized path
     */
    private String getNormalizedPath(String basePath, String relativePath){
        if(!relativePath.startsWith("../")){
            return basePath + relativePath;
        }
        else{
            while(relativePath.startsWith("../")){
                basePath = basePath.substring(0,basePath.substring(0, basePath.length()-1).lastIndexOf("/")+1);
                relativePath = relativePath.substring(3);
            }
            return basePath+relativePath;
        }
    }
}
3
burcakulug

ユーザー「ulab」が別の回答のコメントで指摘しているので、 この回答 (別のスタックオーバーフローの質問へ)で説明されている解決策は、多くの場合に機能します。そのアプローチの大まかな概要は次のとおりです。

SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
URL xsdURL = this.getResource("/xsd/my-schema.xsd");
Schema schema = schemaFactory.newSchema(xsdURL);

このアプローチの鍵は、スキーマファクトリにストリームを渡さずに、URLを渡すことです。このようにして、XSDファイルの場所に関する情報を取得します。

ここで覚えておくべきことの1つは、include要素やimport要素の "schemaLocation"属性が、単純なファイルパスを使用するときにバリデーターに渡したURLを持つXSDファイルのクラスパスの場所を基準にして扱われることです。 「my-common.xsd」または「common/some-concept.xsd」の形式。

注:-上記の例では、スキーマファイルを「xsd」フォルダの下のjarファイルに配置しました。 -「getResource」引数の先頭のスラッシュは、Javaが「this」オブジェクトのパッケージ名ではなくクラスローダーのルートから開始するように指示します。

2

受け入れられた答えは非常に冗長であり、最初にメモリ内にDOMを構築します。相対参照を含め、includesは私にとっては箱から出して動作するようです。

    SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
    Schema schema = schemaFactory.newSchema(new File("../foo.xsd"));
    Validator validator = schema.newValidator();
    validator.validate(new StreamSource(new File("./foo.xml")));
0
teknopaul

私たちにとって、resolveResourceは次のようになりました。いくつかのプロローグ例外と奇妙な要素タイプの後に、「xs:schema」の後に属性指定、 ">"または "/>"を続ける必要があります。要素タイプ "xs:element"の後には、属性指定 ">"または "/>"のいずれかが続く必要があります。 (複数行の内訳のため)

インクルードの構造のため、パス履歴が必要でした

main.xsd (this has include "includes/subPart.xsd")
/includes/subPart.xsd (this has include "./subSubPart.xsd")
/includes/subSubPart.xsd

したがって、コードは次のようになります。

String pathHistory = "";

@Override
public LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId, String baseURI) {
    systemId = systemId.replace("./", "");// we dont need this since getResourceAsStream cannot understand it
    InputStream resourceAsStream = Message.class.getClassLoader().getResourceAsStream(systemId);
    if (resourceAsStream == null) {
        resourceAsStream = Message.class.getClassLoader().getResourceAsStream(pathHistory + systemId);
    } else {
        pathHistory = getNormalizedPath(systemId);
    }
    Scanner s = new Scanner(resourceAsStream).useDelimiter("\\A");
    String s1 = s.next()
            .replaceAll("\\n"," ") //the parser cannot understand elements broken down multiple lines e.g. (<xs:element \n name="buxing">) 
            .replace("\\t", " ") //these two about whitespaces is only for decoration
            .replaceAll("\\s+", " ") 
            .replaceAll("[^\\x20-\\x7e]", ""); //some files has a special character as a first character indicating utf-8 file
    InputStream is = new ByteArrayInputStream(s1.getBytes());

    return new LSInputImpl(publicId, systemId, is);
}

private String getNormalizedPath(String baseURI) {
    return baseURI.substring(0, baseURI.lastIndexOf(System.getProperty("file.separator"))+ 1) ;
}
0
AMegmondoEmber