source: ComponentRegistry/tags/ComponentRegistry-1.11.0/CMDValidate/src/main/java/clarin/cmdi/schema/cmd/Validator.java @ 1843

Last change on this file since 1843 was 1843, checked in by twagoo, 12 years ago

Created branch for ComponentRegistry-1.11.0

Trunk is now 1.12.0

File size: 13.7 KB
Line 
1package clarin.cmdi.schema.cmd;
2
3import clarin.cmdi.xml.Saxon;
4import java.io.File;
5import java.io.IOException;
6import java.io.InputStream;
7import java.net.MalformedURLException;
8import java.net.URL;
9import java.util.List;
10import javax.xml.XMLConstants;
11import javax.xml.parsers.DocumentBuilderFactory;
12import javax.xml.parsers.ParserConfigurationException;
13import javax.xml.transform.Source;
14import javax.xml.transform.dom.DOMSource;
15import javax.xml.transform.stream.StreamSource;
16import javax.xml.validation.Schema;
17import javax.xml.validation.SchemaFactory;
18import net.sf.saxon.s9api.DOMDestination;
19import net.sf.saxon.s9api.QName;
20import net.sf.saxon.s9api.SaxonApiException;
21import net.sf.saxon.s9api.XdmDestination;
22import net.sf.saxon.s9api.XdmItem;
23import net.sf.saxon.s9api.XdmNode;
24import net.sf.saxon.s9api.XsltExecutable;
25import net.sf.saxon.s9api.XsltTransformer;
26import org.w3c.dom.Document;
27import org.xml.sax.SAXException;
28
29/**
30 * The Validator class handles the XSD and Schematron validation of CMD profile and component specifications.
31 *
32 * The class caches thread safe versions of the XSD Schema and the Schematron XSLT.
33 * So multiple instances of this Validator class can be used in parallel and use the same cached schema and transformer.
34 * Although a single instance of the Validator class can't be accessed in parallel it can be used to validate multiple CMD
35 * profiles/components in sequence.
36 *
37 * @author menwin
38 * @author twagoo
39 */
40public class Validator {
41
42    /**
43     * Default location of the CMD schema
44     */
45    static final String CMD_SCHEMA_URL = "http://www.clarin.eu/cmd/general-component-schema.xsd";
46    /**
47     * The immutable location of the CMD schema that is used in this instance
48     */
49    private final URL cmdSchemaUri;
50    /**
51     * The "immutable, and therefore thread-safe," "compiled form of [the Schematron] stylesheet".
52     */
53    private XsltExecutable cmdSchematron = null;
54    /**
55     * The "immutable in-memory representation of [the XSD] grammar".
56     */
57    private Schema cmdSchema = null;
58    /**
59     * The list of validation messages compiled a the last run of the validator.
60     */
61    private List<Message> msgList = null;
62    /**
63     * The Schematron SVRL validation report
64     */
65    private XdmNode validationReport = null;
66
67    /**
68     * Creates a Validator that uses a specific schema specified by its URL
69     *
70     * @param cmdSchemaUri Schema URI to use
71     * @see #CMD_SCHEMA_URL
72     */
73    public Validator(URL cmdSchemaUri) {
74        this.cmdSchemaUri = cmdSchemaUri;
75    }
76
77    /**
78     * Convenience method to build a XSLT transformer from a resource.
79     *
80     * @param uri The location of the resource
81     * @return An executable XSLT
82     * @throws Exception
83     */
84    static XsltExecutable buildTransformer(File file) throws ValidatorException {
85        try {
86            XdmNode xslDoc = Saxon.buildDocument(new javax.xml.transform.stream.StreamSource(file));
87            return Saxon.buildTransformer(xslDoc);
88        } catch (SaxonApiException ex) {
89            throw new ValidatorException(ex);
90        }
91    }
92
93    /**
94     * Convenience method to build a XSLT transformer from a resource.
95     *
96     * @param uri The location of the resource
97     * @return An executable XSLT
98     * @throws Exception
99     */
100    static XsltExecutable buildTransformer(URL url) throws ValidatorException {
101        try {
102            XdmNode xslDoc = Saxon.buildDocument(new javax.xml.transform.stream.StreamSource(url.toExternalForm()));
103            return Saxon.buildTransformer(xslDoc);
104        } catch (SaxonApiException ex) {
105            throw new ValidatorException(ex);
106        }
107
108    }
109
110    /**
111     * Convenience method to build a XSLT transformer from a resource.
112     *
113     * @param uri The location of the resource
114     * @return An executable XSLT
115     * @throws Exception
116     */
117    static XsltExecutable buildTransformer(InputStream stream) throws ValidatorException {
118        try {
119            XdmNode xslDoc = Saxon.buildDocument(new javax.xml.transform.stream.StreamSource(stream));
120            return Saxon.buildTransformer(xslDoc);
121        } catch (SaxonApiException ex) {
122            throw new ValidatorException(ex);
123        }
124
125    }
126
127    /**
128     * Returns the CMD XSD schema, and loads it just-in-time.
129     *
130     * @return An in-memory representation of the grammar
131     * @throws Exception
132     */
133    private synchronized Schema getSchema() throws ValidatorException, IOException {
134        if (cmdSchema == null) {
135            SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
136
137            // Load the CMD XSD.
138            Source schemaFile = new StreamSource(cmdSchemaUri.openStream());
139            try {
140                cmdSchema = factory.newSchema(schemaFile);
141            } catch (SAXException ex) {
142                throw new ValidatorException(ex);
143            }
144        }
145        return cmdSchema;
146    }
147
148    /**
149     * Validation of a loaded CMD profile/component against the XSD schema.
150     *
151     * Unfortunately we can't use the Saxon XSD validator as that is limited to a commercial version of Saxon.
152     *
153     * @param src The loaded CMD profile/component
154     * @return Is the CMD profile/component valid or not?
155     * @throws Exception
156     */
157    public boolean validateXSD(XdmNode src) throws ValidatorException, IOException {
158        try {
159            Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
160            DOMDestination dst = new DOMDestination(doc);
161            Saxon.getProcessor().writeXdmValue(src, dst);
162
163            try {
164                // Create a Validator object, which can be used to validate
165                // an instance document.
166                javax.xml.validation.Validator validator = getSchema().newValidator();
167
168                // Validate the DOM tree.
169                validator.validate(new DOMSource(doc));
170
171            } catch (SAXException e) {
172                Message msg = new Message();
173                msg.error = true;
174                msg.text = e.getMessage();
175                msgList.add(msg);
176                return false;
177            }
178        } catch (SaxonApiException ex) {
179            throw new ValidatorException(ex);
180        } catch (ParserConfigurationException ex) {
181            throw new ValidatorException(ex);
182        }
183
184        return true;
185    }
186
187    /**
188     * Returns the CMD Schematron XSLT, and loads it just-in-time.
189     *
190     * @return The compiled Schematron XSLT
191     * @throws Exception
192     */
193    private synchronized XsltExecutable getSchematron() throws ValidatorException, IOException {
194        if (cmdSchematron == null) {
195            try {
196                // Load the schema
197                XdmNode schema = Saxon.buildDocument(new javax.xml.transform.stream.StreamSource(cmdSchemaUri.openStream()));
198                // Load the Schematron XSL to extract the Schematron rules;
199                XsltTransformer extractSchXsl = buildTransformer(Validator.class.getResource("/schematron/ExtractSchFromXSD-2.xsl")).load();
200                // Load the Schematron XSLs to 'compile' Schematron rules;
201                XsltTransformer includeSchXsl = buildTransformer(Validator.class.getResource("/schematron/iso_dsdl_include.xsl")).load();
202                XsltTransformer expandSchXsl = buildTransformer(Validator.class.getResource("/schematron/iso_abstract_expand.xsl")).load();
203                XsltTransformer compileSchXsl = buildTransformer(Validator.class.getResource("/schematron/iso_svrl_for_xslt2.xsl")).load();
204                // Setup the pipeline
205                XdmDestination destination = new XdmDestination();
206                extractSchXsl.setSource(schema.asSource());
207                extractSchXsl.setDestination(includeSchXsl);
208                includeSchXsl.setDestination(expandSchXsl);
209                expandSchXsl.setDestination(compileSchXsl);
210                compileSchXsl.setDestination(destination);
211                // Extract the Schematron rules from the schema       
212                extractSchXsl.transform();
213                // Compile the Schematron rules XSL
214                cmdSchematron = Saxon.buildTransformer(destination.getXdmNode());
215            } catch (SaxonApiException ex) {
216                throw new ValidatorException(ex);
217            }
218        }
219        return cmdSchematron;
220    }
221
222    /**
223     * Validation of a loaded CMD profile/component against the Schematron XSLT
224     *
225     * @param src The loaded CMD profile/component
226     * @return Is the CMD profile/component valid or not?
227     * @throws Exception
228     */
229    public boolean validateSchematron(XdmNode src) throws ValidatorException, IOException {
230        try {
231            XsltTransformer schematronXsl = getSchematron().load();
232            schematronXsl.setSource(src.asSource());
233            XdmDestination destination = new XdmDestination();
234            schematronXsl.setDestination(destination);
235            schematronXsl.transform();
236
237            validationReport = destination.getXdmNode();
238
239            Saxon.declareXPathNamespace("svrl", "http://purl.oclc.org/dsdl/svrl");
240            return ((net.sf.saxon.value.BooleanValue) Saxon.evaluateXPath(validationReport, "empty(//svrl:failed-assert[(preceding-sibling::svrl:fired-rule)[last()][empty(@role) or @role!='warning']])").evaluateSingle().getUnderlyingValue()).getBooleanValue();
241        } catch (SaxonApiException ex) {
242            throw new ValidatorException(ex);
243        }
244    }
245
246    /**
247     * Validation of a loaded CMD profile/component against both the XSD and the Schematron XSLT
248     *
249     * After validation any messages can be accessed using the {@link getMessages()} method.
250     * Notice that even if a CMD profile/component is valid there might be warning messages.
251     *
252     * @param prof The CMD profile/component
253     * @return Is the CMD profile/component valid or not?
254     * @throws Exception
255     */
256    public boolean validateProfile(Source prof) throws ValidatorException, IOException {
257        // Initalize
258        msgList = new java.util.ArrayList<Message>();
259        validationReport = null;
260
261        try {
262            // load the document
263            XdmNode doc = Saxon.buildDocument(prof);
264
265            // step 1: validate against XML Schema
266            if (!this.validateXSD(doc)) {
267                return false;
268            }
269
270            // step 2: validate Schematron rules
271            return validateSchematron(doc);
272        } catch (SaxonApiException ex) {
273            throw new ValidatorException(ex);
274        }
275
276    }
277
278    /**
279     * Get the list of messages accumulated in the last validation run.
280     *
281     * @return The list of messages
282     * @throws Exception
283     */
284    public List<Message> getMessages() throws ValidatorException {
285        if (validationReport != null) {
286            try {
287                for (XdmItem assertion : Saxon.evaluateXPath(validationReport, "//svrl:failed-assert")) {
288                    Message msg = new Message();
289                    msg.context = Saxon.evaluateXPath(assertion, "(preceding-sibling::svrl:fired-rule)[last()]/@context").evaluateSingle().getStringValue();
290                    msg.test = ((XdmNode) assertion).getAttributeValue(new QName("test"));
291                    msg.location = ((XdmNode) assertion).getAttributeValue(new QName("location"));
292                    msg.error = !((net.sf.saxon.value.BooleanValue) Saxon.evaluateXPath(assertion, "(preceding-sibling::svrl:fired-rule)[last()]/@role='warning'").evaluateSingle().getUnderlyingValue()).getBooleanValue();
293                    msg.text = assertion.getStringValue();
294                    msgList.add(msg);
295                }
296                validationReport = null;
297            } catch (SaxonApiException ex) {
298                throw new ValidatorException(ex);
299            }
300        }
301        return msgList;
302    }
303
304    /**
305     * Print the list of messages accumulated in the last validation run.
306     *
307     * @param out
308     * @throws Exception
309     */
310    public void printMessages(java.io.PrintStream out) throws Exception {
311        for (Message msg : getMessages()) {
312            out.println("" + (msg.isError() ? "ERROR" : "WARNING") + (msg.getLocation() != null ? " at " + msg.getLocation() : ""));
313            out.println("  " + msg.getText());
314        }
315    }
316
317    /**
318     * @param args One or more CMD profile/component files to validate.
319     */
320    public static void main(String[] args) {
321        try {
322
323            URL schemaURL = null;
324
325            int startArg = 0;
326            if (args.length > 0) {
327                if ("-s".equals(args[0].trim())) {
328                    if (args.length > 1) {
329                        String schemaArg = args[1];
330                        System.err.println("Using schema URL" + schemaArg);
331                        schemaURL = new URL(schemaArg);
332                        startArg = 2;
333                    } else {
334                        printUsage(args);
335                        return;
336                    }
337                }
338            } else {
339                printUsage(args);
340                return;
341            }
342
343            if (schemaURL == null) {
344                schemaURL = new URL(CMD_SCHEMA_URL);
345            }
346
347            final Validator cmdValidator = new Validator(schemaURL);
348
349            for (int i = startArg; i < args.length; i++) {
350                String f = args[i];
351                System.out.print("CMD validate[" + f + "] ");
352                try {
353                    Source src = new javax.xml.transform.stream.StreamSource(new java.io.File(f));
354                    if (cmdValidator.validateProfile(src)) {
355                        System.out.println("valid");
356                    } else {
357                        System.out.println("invalid");
358                    }
359                    cmdValidator.printMessages(System.out);
360                } catch (Exception e) {
361                    System.err.println("failed:");
362                    e.printStackTrace(System.out);
363                }
364            }
365        } catch (MalformedURLException e) {
366            System.err.println("failed:");
367            e.printStackTrace(System.out);
368        }
369    }
370
371    private static void printUsage(String[] args) {
372        System.err.println("Arguments: [-s schemafileURL] files...");
373    }
374
375    /**
376     * Public inner class to represent validation messages.
377     */
378    public final static class Message {
379
380        /**
381         * Is the message and error or an warning?
382         */
383        boolean error = false;
384        /**
385         * The context of the message (might be null).
386         */
387        String context = null;
388        /**
389         * The test that triggered the message (might be null).
390         */
391        String test = null;
392        /**
393         * The location that triggered the test (might be null).
394         */
395        String location = null;
396        /**
397         * The actual message.
398         */
399        String text = null;
400
401        /**
402         * @return the error
403         */
404        public boolean isError() {
405            return error;
406        }
407
408        /**
409         * @return the context
410         */
411        public String getContext() {
412            return context;
413        }
414
415        /**
416         * @return the test
417         */
418        public String getTest() {
419            return test;
420        }
421
422        /**
423         * @return the location
424         */
425        public String getLocation() {
426            return location;
427        }
428
429        /**
430         * @return the text
431         */
432        public String getText() {
433            return text;
434        }
435    }
436}
Note: See TracBrowser for help on using the repository browser.