/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.cocoon.transformation; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.avalon.framework.activity.Disposable; import org.apache.avalon.framework.configuration.Configurable; import org.apache.avalon.framework.configuration.Configuration; import org.apache.avalon.framework.configuration.ConfigurationException; import org.apache.avalon.framework.parameters.Parameters; import org.apache.avalon.framework.service.ServiceException; import org.apache.avalon.framework.service.ServiceManager; import org.apache.avalon.framework.service.Serviceable; import org.apache.cocoon.ProcessingException; import org.apache.cocoon.caching.CacheableProcessingComponent; import org.apache.cocoon.components.validation.ValidationHandler; import org.apache.cocoon.components.validation.Validator; import org.apache.cocoon.environment.SourceResolver; import org.apache.cocoon.xml.XMLConsumer; import org.apache.excalibur.source.Source; import org.apache.excalibur.source.SourceValidity; import org.xml.sax.ContentHandler; import org.xml.sax.ErrorHandler; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.helpers.AttributesImpl; /** * <p>The {@link ValidationReportTransformer} provides a {@link Transformer} * validating documents while being processed in a Cocoon pipeline, and preparing * a report of all detected inconsistancies according the specified schema.</p> * * <p>The only defined (but not required) configuration for this component is * <code><grammar><i>...string...</i></grammar></code> * indicating the default grammar language of the schemas to use.</p> * * <p>This configuration parameter can be overridden by specifying the * <code>grammar</code> parameter when using this {@link Transformer} in a * pipeline.</p> * * <p>If no grammar is specified (either as a configuration, or a parameter) this * transformer will instruct the {@link Validator} to try and guess the grammar * of the schema being parsed.</p> * * <p>The report prepared by this transformer will look like the following:</p> * * <pre> * <report xmlns="http://apache.org/cocoon/validation/1.0"> * <warning system="..." public="..." line="..." column="..."> * ... detailed message ... * </warning> * <error system="..." public="..." line="..." column="..."> * ... detailed message ... * </error> * <fatal system="..." public="..." line="..." column="..."> * ... message ... * </fatal> * </report> * </pre> * * <p>The location attributes specified in the <code><warning/></code>, * <code><error/></code> and <code><fatal/></code> tags are all optional * and will be generated only if known.</p> * */ public class ValidationReportTransformer extends AbstractTransformer implements Configurable, Serviceable, Disposable, CacheableProcessingComponent { /** <p>The configured {@link ServiceManager} instance.</p> */ private ServiceManager serviceManager = null; /** <p>The configured {@link Validator} instance.</p> */ private Validator validator = null; /** <p>The configured default grammar language.</p> */ private String grammar = null; /** <p>The {@link Report} instance to be used while processing a request.</p> */ private Report report = null; /** <p>The {@link ValidationHandler} to use in this transformation.</p> */ private ValidationHandler handler = null; /** <p>The {@link XMLConsumer} to send the validation report to.</p> */ private XMLConsumer consumer = null; /** <p>A unique key identifying the schema source for caching.</p> */ private String key = null; /** * <p>Create a new {@link ValidationReportTransformer} instance.</p> */ public ValidationReportTransformer() { super(); } /** * <p>Contextualize this component instance specifying its associated * {@link ServiceManager} instance.</p> * * @param manager the {@link ServiceManager} to associate with this component. * @throws ServiceException if a dependancy of this could not be resolved. */ public void service(ServiceManager manager) throws ServiceException { this.serviceManager = manager; this.validator = (Validator) manager.lookup(Validator.ROLE); } /** * <p>Configure this component instance.</p> * * <p>The only defined (but not required) configuration for this component is * <code><grammar><i>...string...</i></grammar></code> * indicating the default grammar used by this transformer used for parsing * schemas.</p> * * @param configuration a {@link Configuration} instance for this component. * @throws ConfigurationException never thrown. */ public void configure(Configuration configuration) throws ConfigurationException { this.grammar = configuration.getChild("grammar").getValue(null); } /** * <p>Dispose of this component instance releasing all previously acquired * required instances back to the {@link ServiceManager}.</p> */ public void dispose() { this.serviceManager.release(this.validator); } /** * <p>Contextualize this component in the scope of a pipeline when a request * is processed.</p> * * @param resolver the {@link SourceResolver} contextualized in this request. * @param objectModel unused. * @param source the source URI of the schema to validate against. * @param parameters unused. */ public void setup(SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws ProcessingException, SAXException, IOException { Source s = null; try { Report r = new Report(); String g = parameters.getParameter("grammar", this.grammar); s = resolver.resolveURI(source); if (g == null) { this.handler = this.validator.getValidationHandler(s, r); } else{ this.handler = this.validator.getValidationHandler(s, g, r); } this.setContentHandler(this.handler); this.setLexicalHandler(this.handler); this.report = r; } finally { if (source != null) resolver.release(s); } } /** * <p>Specify the {@link XMLConsumer} receiving SAX events emitted by this * {@link Transformer} instance in the scope of a request.</p> * * @param consumer the {@link XMLConsumer} to send SAX events to. */ public void setConsumer(XMLConsumer consumer) { this.consumer = consumer; } /** * <p>Receive notification of the end of the document and produce the report * of the validation result.</p> * * @throws SAXException * @see org.xml.sax.ContentHandler#endDocument() */ public void endDocument() throws SAXException { super.endDocument(); this.report.generateReport(this.consumer); } /** * <p>Return the unique key to associated with the schema being processed in * the scope of the request being processed for caching.</p> * * @return a non null {@link String} representing the unique key for the schema. */ public Serializable getKey() { return this.key; } /** * <p>Return the {@link SourceValidity} associated with the schema currently * being processed in the scope of the request being processed.</p> * * @return a non null {@link SourceValidity} instance. */ public SourceValidity getValidity() { return this.handler.getValidity(); } /** * <p>Recycle this component instance at the end of request processing.</p> */ public void recycle() { this.consumer = null; this.handler = null; this.report = null; this.key = null; super.recycle(); } /** * */ private static final class Report implements ErrorHandler { private static final String NS = "http://apache.org/cocoon/validation/1.0"; private final List entries = new ArrayList(); public void warning(SAXParseException exception) throws SAXException { this.entries.add(new ReportEntry("warning", exception)); } public void error(SAXParseException exception) throws SAXException { this.entries.add(new ReportEntry("error", exception)); } public void fatalError(SAXParseException exception) throws SAXException { this.entries.add(new ReportEntry("fatal", exception)); } private void generateReport(ContentHandler handler) throws SAXException { /* Start the report */ handler.startDocument(); handler.startPrefixMapping("", NS); AttributesImpl attributes = new AttributesImpl(); handler.startElement(NS, "report", "report", attributes); /* Each collected error will generate its own entry */ Iterator iterator = this.entries.iterator(); while(iterator.hasNext()) { ReportEntry entry = (ReportEntry) iterator.next(); attributes.clear(); if (entry.exception.getPublicId() != null) { if (! "".equals(entry.exception.getPublicId())) { attributes.addAttribute("", "public", "public", "CDATA", entry.exception.getPublicId()); } } if (entry.exception.getSystemId() != null) { if (! "".equals(entry.exception.getSystemId())) { attributes.addAttribute("", "system", "system", "CDATA", entry.exception.getSystemId()); } } if (entry.exception.getLineNumber() >= 0) { String l = Integer.toString(entry.exception.getLineNumber()); attributes.addAttribute("", "line", "line", "CDATA", l); } if (entry.exception.getColumnNumber() >= 0) { String c = Integer.toString(entry.exception.getColumnNumber()); attributes.addAttribute("", "column", "column", "CDATA", c); } String level = entry.level; handler.startElement(NS, level, level, attributes); char message[] = entry.exception.getMessage().toCharArray(); handler.characters(message, 0, message.length); handler.endElement(NS, level, level); } /* After all the exceptions have been dumped, close the report */ handler.endElement(NS, "report", "report"); handler.endPrefixMapping(""); handler.endDocument(); } } private static final class ReportEntry { private final String level; private final SAXParseException exception; private ReportEntry(String level, SAXParseException exception) { this.level = level; this.exception = exception; } } }