001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.transformation;
018:
019: import java.io.IOException;
020: import java.io.Serializable;
021: import java.util.ArrayList;
022: import java.util.Iterator;
023: import java.util.List;
024: import java.util.Map;
025:
026: import org.apache.avalon.framework.activity.Disposable;
027: import org.apache.avalon.framework.configuration.Configurable;
028: import org.apache.avalon.framework.configuration.Configuration;
029: import org.apache.avalon.framework.configuration.ConfigurationException;
030: import org.apache.avalon.framework.parameters.Parameters;
031: import org.apache.avalon.framework.service.ServiceException;
032: import org.apache.avalon.framework.service.ServiceManager;
033: import org.apache.avalon.framework.service.Serviceable;
034: import org.apache.cocoon.ProcessingException;
035: import org.apache.cocoon.caching.CacheableProcessingComponent;
036: import org.apache.cocoon.components.validation.ValidationHandler;
037: import org.apache.cocoon.components.validation.Validator;
038: import org.apache.cocoon.environment.SourceResolver;
039: import org.apache.cocoon.xml.XMLConsumer;
040: import org.apache.excalibur.source.Source;
041: import org.apache.excalibur.source.SourceValidity;
042: import org.xml.sax.ContentHandler;
043: import org.xml.sax.ErrorHandler;
044: import org.xml.sax.SAXException;
045: import org.xml.sax.SAXParseException;
046: import org.xml.sax.helpers.AttributesImpl;
047:
048: /**
049: * <p>The {@link ValidationReportTransformer} provides a {@link Transformer}
050: * validating documents while being processed in a Cocoon pipeline, and preparing
051: * a report of all detected inconsistancies according the specified schema.</p>
052: *
053: * <p>The only defined (but not required) configuration for this component is
054: * <code><grammar><i>...string...</i></grammar></code>
055: * indicating the default grammar language of the schemas to use.</p>
056: *
057: * <p>This configuration parameter can be overridden by specifying the
058: * <code>grammar</code> parameter when using this {@link Transformer} in a
059: * pipeline.</p>
060: *
061: * <p>If no grammar is specified (either as a configuration, or a parameter) this
062: * transformer will instruct the {@link Validator} to try and guess the grammar
063: * of the schema being parsed.</p>
064: *
065: * <p>The report prepared by this transformer will look like the following:</p>
066: *
067: * <pre>
068: * <report xmlns="http://apache.org/cocoon/validation/1.0">
069: * <warning system="..." public="..." line="..." column="...">
070: * ... detailed message ...
071: * </warning>
072: * <error system="..." public="..." line="..." column="...">
073: * ... detailed message ...
074: * </error>
075: * <fatal system="..." public="..." line="..." column="...">
076: * ... message ...
077: * </fatal>
078: * </report>
079: * </pre>
080: *
081: * <p>The location attributes specified in the <code><warning/></code>,
082: * <code><error/></code> and <code><fatal/></code> tags are all optional
083: * and will be generated only if known.</p>
084: *
085: */
086: public class ValidationReportTransformer extends AbstractTransformer
087: implements Configurable, Serviceable, Disposable,
088: CacheableProcessingComponent {
089:
090: /** <p>The configured {@link ServiceManager} instance.</p> */
091: private ServiceManager serviceManager = null;
092: /** <p>The configured {@link Validator} instance.</p> */
093: private Validator validator = null;
094: /** <p>The configured default grammar language.</p> */
095: private String grammar = null;
096:
097: /** <p>The {@link Report} instance to be used while processing a request.</p> */
098: private Report report = null;
099: /** <p>The {@link ValidationHandler} to use in this transformation.</p> */
100: private ValidationHandler handler = null;
101: /** <p>The {@link XMLConsumer} to send the validation report to.</p> */
102: private XMLConsumer consumer = null;
103: /** <p>A unique key identifying the schema source for caching.</p> */
104: private String key = null;
105:
106: /**
107: * <p>Create a new {@link ValidationReportTransformer} instance.</p>
108: */
109: public ValidationReportTransformer() {
110: super ();
111: }
112:
113: /**
114: * <p>Contextualize this component instance specifying its associated
115: * {@link ServiceManager} instance.</p>
116: *
117: * @param manager the {@link ServiceManager} to associate with this component.
118: * @throws ServiceException if a dependancy of this could not be resolved.
119: */
120: public void service(ServiceManager manager) throws ServiceException {
121: this .serviceManager = manager;
122: this .validator = (Validator) manager.lookup(Validator.ROLE);
123: }
124:
125: /**
126: * <p>Configure this component instance.</p>
127: *
128: * <p>The only defined (but not required) configuration for this component is
129: * <code><grammar><i>...string...</i></grammar></code>
130: * indicating the default grammar used by this transformer used for parsing
131: * schemas.</p>
132: *
133: * @param configuration a {@link Configuration} instance for this component.
134: * @throws ConfigurationException never thrown.
135: */
136: public void configure(Configuration configuration)
137: throws ConfigurationException {
138: this .grammar = configuration.getChild("grammar").getValue(null);
139: }
140:
141: /**
142: * <p>Dispose of this component instance releasing all previously acquired
143: * required instances back to the {@link ServiceManager}.</p>
144: */
145: public void dispose() {
146: this .serviceManager.release(this .validator);
147: }
148:
149: /**
150: * <p>Contextualize this component in the scope of a pipeline when a request
151: * is processed.</p>
152: *
153: * @param resolver the {@link SourceResolver} contextualized in this request.
154: * @param objectModel unused.
155: * @param source the source URI of the schema to validate against.
156: * @param parameters unused.
157: */
158: public void setup(SourceResolver resolver, Map objectModel,
159: String source, Parameters parameters)
160: throws ProcessingException, SAXException, IOException {
161: Source s = null;
162: try {
163: Report r = new Report();
164: String g = parameters.getParameter("grammar", this .grammar);
165: s = resolver.resolveURI(source);
166: if (g == null) {
167: this .handler = this .validator
168: .getValidationHandler(s, r);
169: } else {
170: this .handler = this .validator.getValidationHandler(s,
171: g, r);
172: }
173: this .setContentHandler(this .handler);
174: this .setLexicalHandler(this .handler);
175: this .report = r;
176: } finally {
177: if (source != null)
178: resolver.release(s);
179: }
180: }
181:
182: /**
183: * <p>Specify the {@link XMLConsumer} receiving SAX events emitted by this
184: * {@link Transformer} instance in the scope of a request.</p>
185: *
186: * @param consumer the {@link XMLConsumer} to send SAX events to.
187: */
188: public void setConsumer(XMLConsumer consumer) {
189: this .consumer = consumer;
190: }
191:
192: /**
193: * <p>Receive notification of the end of the document and produce the report
194: * of the validation result.</p>
195: *
196: * @throws SAXException
197: * @see org.xml.sax.ContentHandler#endDocument()
198: */
199: public void endDocument() throws SAXException {
200: super .endDocument();
201: this .report.generateReport(this .consumer);
202: }
203:
204: /**
205: * <p>Return the unique key to associated with the schema being processed in
206: * the scope of the request being processed for caching.</p>
207: *
208: * @return a non null {@link String} representing the unique key for the schema.
209: */
210: public Serializable getKey() {
211: return this .key;
212: }
213:
214: /**
215: * <p>Return the {@link SourceValidity} associated with the schema currently
216: * being processed in the scope of the request being processed.</p>
217: *
218: * @return a non null {@link SourceValidity} instance.
219: */
220: public SourceValidity getValidity() {
221: return this .handler.getValidity();
222: }
223:
224: /**
225: * <p>Recycle this component instance at the end of request processing.</p>
226: */
227: public void recycle() {
228: this .consumer = null;
229: this .handler = null;
230: this .report = null;
231: this .key = null;
232: super .recycle();
233: }
234:
235: /**
236: *
237: */
238: private static final class Report implements ErrorHandler {
239:
240: private static final String NS = "http://apache.org/cocoon/validation/1.0";
241: private final List entries = new ArrayList();
242:
243: public void warning(SAXParseException exception)
244: throws SAXException {
245: this .entries.add(new ReportEntry("warning", exception));
246: }
247:
248: public void error(SAXParseException exception)
249: throws SAXException {
250: this .entries.add(new ReportEntry("error", exception));
251: }
252:
253: public void fatalError(SAXParseException exception)
254: throws SAXException {
255: this .entries.add(new ReportEntry("fatal", exception));
256: }
257:
258: private void generateReport(ContentHandler handler)
259: throws SAXException {
260: /* Start the report */
261: handler.startDocument();
262: handler.startPrefixMapping("", NS);
263: AttributesImpl attributes = new AttributesImpl();
264: handler.startElement(NS, "report", "report", attributes);
265:
266: /* Each collected error will generate its own entry */
267: Iterator iterator = this .entries.iterator();
268: while (iterator.hasNext()) {
269: ReportEntry entry = (ReportEntry) iterator.next();
270: attributes.clear();
271:
272: if (entry.exception.getPublicId() != null) {
273: if (!"".equals(entry.exception.getPublicId())) {
274: attributes.addAttribute("", "public", "public",
275: "CDATA", entry.exception.getPublicId());
276: }
277: }
278:
279: if (entry.exception.getSystemId() != null) {
280: if (!"".equals(entry.exception.getSystemId())) {
281: attributes.addAttribute("", "system", "system",
282: "CDATA", entry.exception.getSystemId());
283: }
284: }
285:
286: if (entry.exception.getLineNumber() >= 0) {
287: String l = Integer.toString(entry.exception
288: .getLineNumber());
289: attributes.addAttribute("", "line", "line",
290: "CDATA", l);
291: }
292:
293: if (entry.exception.getColumnNumber() >= 0) {
294: String c = Integer.toString(entry.exception
295: .getColumnNumber());
296: attributes.addAttribute("", "column", "column",
297: "CDATA", c);
298: }
299:
300: String level = entry.level;
301: handler.startElement(NS, level, level, attributes);
302: char message[] = entry.exception.getMessage()
303: .toCharArray();
304: handler.characters(message, 0, message.length);
305: handler.endElement(NS, level, level);
306: }
307:
308: /* After all the exceptions have been dumped, close the report */
309: handler.endElement(NS, "report", "report");
310: handler.endPrefixMapping("");
311: handler.endDocument();
312: }
313: }
314:
315: private static final class ReportEntry {
316:
317: private final String level;
318: private final SAXParseException exception;
319:
320: private ReportEntry(String level, SAXParseException exception) {
321: this.level = level;
322: this.exception = exception;
323: }
324: }
325: }
|