001    /*
002     * Copyright (c) 2005, 2006, 2007 Henri Sivonen
003     * Copyright (c) 2007 Mozilla Foundation
004     *
005     * Permission is hereby granted, free of charge, to any person obtaining a 
006     * copy of this software and associated documentation files (the "Software"), 
007     * to deal in the Software without restriction, including without limitation 
008     * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
009     * and/or sell copies of the Software, and to permit persons to whom the 
010     * Software is furnished to do so, subject to the following conditions:
011     *
012     * The above copyright notice and this permission notice shall be included in 
013     * all copies or substantial portions of the Software.
014     *
015     * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
016     * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
017     * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
018     * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
019     * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
020     * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
021     * DEALINGS IN THE SOFTWARE.
022     */
023    
024    package nu.validator.messages;
025    
026    import java.io.IOException;
027    
028    import nu.validator.messages.types.MessageType;
029    import nu.validator.source.Location;
030    import nu.validator.source.SourceCode;
031    import nu.validator.source.SourceHandler;
032    import nu.validator.xml.CharacterUtil;
033    
034    import org.whattf.checker.NormalizationChecker;
035    import org.xml.sax.ErrorHandler;
036    import org.xml.sax.SAXException;
037    import org.xml.sax.SAXParseException;
038    
039    import com.ibm.icu.text.Normalizer;
040    
041    
042    public final class MessageEmitterAdapter implements ErrorHandler {
043        
044        private final static char[] INDETERMINATE_MESSAGE = "The result cannot be determined due to a non-document-error.".toCharArray();
045    
046        private int warnings = 0;
047    
048        private int errors = 0;
049    
050        private int fatalErrors = 0;
051    
052        private int nonDocumentErrors = 0;
053        
054        private final SourceCode sourceCode;
055        
056        private final MessageEmitter emitter;
057        
058        private final ExactErrorHandler exactErrorHandler;
059        
060        private final boolean showSource;
061        
062        protected static String scrub(String s) throws SAXException {
063            if (s == null) {
064                return null;
065            }
066            s = CharacterUtil.prudentlyScrubCharacterData(s);
067            if (NormalizationChecker.startsWithComposingChar(s)) {
068                s = " " + s;
069            }
070            return Normalizer.normalize(s, Normalizer.NFC, 0);
071        }
072    
073        public MessageEmitterAdapter(SourceCode sourceCode, boolean showSource, MessageEmitter messageEmitter) {
074            super();
075            this.sourceCode = sourceCode;
076            this.emitter = messageEmitter;
077            this.exactErrorHandler = new ExactErrorHandler(this);
078            this.showSource = showSource;
079        }
080    
081        /**
082         * @return Returns the errors.
083         */
084        public int getErrors() {
085            return errors;
086        }
087    
088        /**
089         * @return Returns the fatalErrors.
090         */
091        public int getFatalErrors() {
092            return fatalErrors;
093        }
094    
095        /**
096         * @return Returns the warnings.
097         */
098        public int getWarnings() {
099            return warnings;
100        }
101    
102        private boolean isErrors() {
103            return !(errors == 0 && fatalErrors == 0);
104        }
105    
106        /**
107         * @see org.xml.sax.ErrorHandler#warning(org.xml.sax.SAXParseException)
108         */
109        public void warning(SAXParseException e) throws SAXException {
110            warning(e, false);
111        }
112    
113        /**
114         * @param e
115         * @throws SAXException
116         */
117        private void warning(SAXParseException e, boolean exact) throws SAXException {
118            if (fatalErrors > 0 || nonDocumentErrors > 0) {
119                return;
120            }
121            this.warnings++;
122            messageFromSAXParseException(MessageType.WARNING, e, exact);
123        }
124    
125        /**
126         * @see org.xml.sax.ErrorHandler#error(org.xml.sax.SAXParseException)
127         */
128        public void error(SAXParseException e) throws SAXException {
129            error(e, false);
130        }
131    
132        /**
133         * @param e
134         * @throws SAXException
135         */
136        private void error(SAXParseException e, boolean exact) throws SAXException {
137            if (fatalErrors > 0 || nonDocumentErrors > 0) {
138                return;
139            }
140            this.errors++;
141            messageFromSAXParseException(MessageType.ERROR, e, exact);
142        }
143    
144        /**
145         * @see org.xml.sax.ErrorHandler#fatalError(org.xml.sax.SAXParseException)
146         */
147        public void fatalError(SAXParseException e) throws SAXException {
148            fatalError(e, false);
149        }
150    
151        /**
152         * @param e
153         * @throws SAXException
154         */
155        private void fatalError(SAXParseException e, boolean exact) throws SAXException {
156            if (fatalErrors > 0 || nonDocumentErrors > 0) {
157                return;
158            }
159            this.fatalErrors++;
160            Exception wrapped = e.getException();
161            if (wrapped instanceof IOException) {
162                message(MessageType.IO, ((IOException) wrapped).getMessage(), null, -1, -1 , false);
163            } else {
164                messageFromSAXParseException(MessageType.FATAL, e, exact);
165            }
166        }
167    
168        /**
169         * @see nu.validator.servlet.InfoErrorHandler#info(java.lang.String)
170         */
171        public void info(String str) throws SAXException {
172            message(MessageType.INFO, str, null, -1, -1 , false);
173        }
174    
175        /**
176         * @see nu.validator.servlet.InfoErrorHandler#ioError(java.io.IOException)
177         */
178        public void ioError(IOException e) throws SAXException {
179            this.nonDocumentErrors++;
180            message(MessageType.IO, e.getMessage(), null, -1, -1 , false);
181        }
182    
183        /**
184         * @see nu.validator.servlet.InfoErrorHandler#internalError(java.lang.Throwable,
185         *      java.lang.String)
186         */
187        public void internalError(Throwable e, String message) throws SAXException {
188            this.nonDocumentErrors++;
189            message(MessageType.INTERNAL, message, null, -1, -1 , false);
190        }
191    
192        /**
193         * @see nu.validator.servlet.InfoErrorHandler#schemaError(java.lang.Exception)
194         */
195        public void schemaError(Exception e) throws SAXException {
196            this.nonDocumentErrors++;
197            message(MessageType.SCHEMA, e.getMessage(), null, -1, -1 , false);
198        }
199    
200        /**
201         * @see nu.validator.servlet.InfoErrorHandler#start()
202         */
203        public void start(String documentUri) throws SAXException {
204            emitter.startMessages(scrub(documentUri), showSource);
205        }
206        
207        /**
208         * @see nu.validator.servlet.InfoErrorHandler#end()
209         */
210        public void end(String successMessage, String failureMessage)
211                throws SAXException {
212            ResultHandler resultHandler = emitter.startResult();
213            if (resultHandler != null) {
214                if (isIndeterminate()) {
215                    resultHandler.startResult(Result.INDETERMINATE);
216                    resultHandler.characters(INDETERMINATE_MESSAGE, 0, INDETERMINATE_MESSAGE.length);
217                    resultHandler.endResult();
218                } else if (isErrors()) {
219                    resultHandler.startResult(Result.FAILURE);
220                    resultHandler.characters(failureMessage.toCharArray(), 0, failureMessage.length());
221                    resultHandler.endResult();                
222                } else {
223                    resultHandler.startResult(Result.SUCCESS);
224                    resultHandler.characters(successMessage.toCharArray(), 0, successMessage.length());
225                    resultHandler.endResult();                                
226                }
227            }
228            emitter.endResult();
229            
230            if (showSource) {
231                SourceHandler sourceHandler = emitter.startFullSource();
232                if (sourceHandler != null) {
233                    sourceCode.emitSource(sourceHandler);
234                }
235                emitter.endFullSource();
236            }
237            emitter.endMessages();
238        }
239    
240        private boolean isIndeterminate() {
241            return nonDocumentErrors > 0;
242        }
243    
244        private void messageFromSAXParseException(MessageType type, SAXParseException spe, boolean exact) throws SAXException {
245            message(type, spe.getMessage(), spe.getSystemId(), spe.getLineNumber(), spe.getColumnNumber(), exact);
246        }
247        
248        private void message(MessageType type, String message, String systemId, int oneBasedLine, int oneBasedColumn, boolean exact) throws SAXException {
249            String uri = sourceCode.getUri();
250            if (oneBasedLine > -1 && (uri == systemId || (uri != null && uri.equals(systemId)))) {
251                if (oneBasedColumn > -1) {
252                    if (exact) {
253                        messageWithExact(type, message, oneBasedLine, oneBasedColumn);
254                    } else {
255                        messageWithRange(type, message, oneBasedLine, oneBasedColumn);
256                    }
257                } else {
258                    messageWithLine(type, message, oneBasedLine);
259                }
260            } else {
261                messageWithoutExtract(type, message, systemId, oneBasedLine, oneBasedColumn);   
262            }
263        }
264    
265        private void messageWithRange(MessageType type, String message, int oneBasedLine, int oneBasedColumn) throws SAXException {
266            Location rangeLast = sourceCode.newLocatorLocation(oneBasedLine, oneBasedColumn);
267            if (!sourceCode.isWithinKnownSource(rangeLast)) {
268                messageWithoutExtract(type, message, null, oneBasedLine, oneBasedColumn);
269                return;
270            }
271            Location rangeStart = sourceCode.rangeStartForRangeLast(rangeLast);
272            emitter.startMessage(type, null, rangeStart.getLine() + 1, rangeStart.getColumn() + 1, oneBasedLine, oneBasedColumn, false);
273            messageText(message);
274            SourceHandler sourceHandler = emitter.startSource();
275            if (sourceHandler != null) {
276                sourceCode.rangeEndError(rangeStart, rangeLast, sourceHandler);
277            }
278            emitter.endSource();
279            // XXX elaboration
280            emitter.endMessage();
281        }
282    
283        private void messageWithExact(MessageType type, String message, int oneBasedLine, int oneBasedColumn) throws SAXException {
284            emitter.startMessage(type, null, oneBasedLine, oneBasedColumn, oneBasedLine, oneBasedColumn, true);
285            messageText(message);
286            Location location = sourceCode.newLocatorLocation(oneBasedLine, oneBasedColumn);
287            if (sourceCode.isWithinKnownSource(location)) {
288                SourceHandler sourceHandler = emitter.startSource();
289                if (sourceHandler != null) {
290                    sourceCode.exactError(location, sourceHandler);
291                }
292                emitter.endSource();
293            } else {
294                sourceCode.rememberExactError(location);
295            }
296            // XXX elaboration
297            emitter.endMessage();
298        }
299    
300        private void messageWithLine(MessageType type, String message, int oneBasedLine) throws SAXException {
301            if (!sourceCode.isWithinKnownSource(oneBasedLine)) {
302                throw new RuntimeException("Bug. Line out of range!");
303            }
304            emitter.startMessage(type, null, oneBasedLine, -1, oneBasedLine, -1, false);
305            messageText(message);
306            SourceHandler sourceHandler = emitter.startSource();
307            if (sourceHandler != null) {
308                sourceCode.lineError(oneBasedLine, sourceHandler);
309            }
310            emitter.endSource();
311            // XXX elaboration
312            emitter.endMessage();
313        }
314    
315        private void messageWithoutExtract(MessageType type, String message, String systemId, int oneBasedLine, int oneBasedColumn) throws SAXException {
316            emitter.startMessage(type, scrub(systemId), oneBasedLine, oneBasedColumn, oneBasedLine, oneBasedColumn, false);
317            messageText(message);
318            // XXX elaboration
319            emitter.endMessage();
320        }
321    
322        /**
323         * @param message
324         * @throws SAXException
325         */
326        private void messageText(String message) throws SAXException {
327            MessageTextHandler messageTextHandler = emitter.startText();
328            if (messageTextHandler != null) {
329                emitStringWithQurlyQuotes(message, messageTextHandler);
330            }
331            emitter.endText();
332        }
333        
334        private void emitStringWithQurlyQuotes(String message, MessageTextHandler messageTextHandler) throws SAXException {
335            if (message == null) {
336                message = "";
337            }
338            message = scrub(message);
339            int len = message.length();
340            int start = 0;
341            int startQuotes = 0;
342            for (int i = 0; i < len; i++) {
343                char c = message.charAt(i);
344                if (c == '\u201C') {
345                    startQuotes++;
346                    if (startQuotes == 1) {
347                        char[] scrubbed = scrub(message.substring(start, i)).toCharArray();
348                        messageTextHandler.characters(scrubbed, 0, scrubbed.length);
349                        start = i + 1;
350                        messageTextHandler.startCode();
351                    }
352                } else if (c == '\u201D' && startQuotes > 0) {
353                    startQuotes--;
354                    if (startQuotes == 0) {
355                        char[] scrubbed = scrub(message.substring(start, i)).toCharArray();
356                        messageTextHandler.characters(scrubbed, 0, scrubbed.length);
357                        start = i + 1;
358                        messageTextHandler.endCode();                    
359                    }
360                }
361            }
362            if (start < len) {
363                char[] scrubbed = scrub(message.substring(start, len)).toCharArray();
364                messageTextHandler.characters(scrubbed, 0, scrubbed.length);
365            }
366            if (startQuotes > 0) {
367                messageTextHandler.endCode();                                
368            }
369        }
370        
371        private final class ExactErrorHandler implements ErrorHandler {
372    
373            private final MessageEmitterAdapter owner;
374    
375            /**
376             * @param owner
377             */
378            ExactErrorHandler(final MessageEmitterAdapter owner) {
379                this.owner = owner;
380            }
381    
382            public void error(SAXParseException exception) throws SAXException {
383                owner.error(exception, true);
384            }
385    
386            public void fatalError(SAXParseException exception) throws SAXException {
387                owner.fatalError(exception, true);
388            }
389    
390            public void warning(SAXParseException exception) throws SAXException {
391                owner.warning(exception, true);
392            }
393            
394        }
395    
396        /**
397         * Returns the exactErrorHandler.
398         * 
399         * @return the exactErrorHandler
400         */
401        public ErrorHandler getExactErrorHandler() {
402            return exactErrorHandler;
403        }
404    }