001: /*
002: * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
003: *
004: * This file is part of Resin(R) Open Source
005: *
006: * Each copy or derived work must preserve the copyright notice and this
007: * notice unmodified.
008: *
009: * Resin Open Source is free software; you can redistribute it and/or modify
010: * it under the terms of the GNU General Public License as published by
011: * the Free Software Foundation; either version 2 of the License, or
012: * (at your option) any later version.
013: *
014: * Resin Open Source is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
017: * of NON-INFRINGEMENT. See the GNU General Public License for more
018: * details.
019: *
020: * You should have received a copy of the GNU General Public License
021: * along with Resin Open Source; if not, write to the
022: *
023: * Free Software Foundation, Inc.
024: * 59 Temple Place, Suite 330
025: * Boston, MA 02111-1307 USA
026: *
027: * @author Scott Ferguson
028: */
029:
030: package com.caucho.relaxng;
031:
032: import com.caucho.relaxng.program.EmptyItem;
033: import com.caucho.relaxng.program.Item;
034: import com.caucho.util.CharBuffer;
035: import com.caucho.util.L10N;
036: import com.caucho.util.LruCache;
037: import com.caucho.vfs.Path;
038: import com.caucho.vfs.ReadStream;
039: import com.caucho.vfs.Vfs;
040: import com.caucho.xml.QName;
041:
042: import org.xml.sax.Attributes;
043: import org.xml.sax.ErrorHandler;
044: import org.xml.sax.Locator;
045: import org.xml.sax.SAXException;
046: import org.xml.sax.SAXParseException;
047: import org.xml.sax.helpers.DefaultHandler;
048:
049: import java.io.IOException;
050: import java.util.ArrayList;
051: import java.util.Collections;
052: import java.util.HashSet;
053: import java.util.LinkedHashSet;
054: import java.util.logging.Level;
055: import java.util.logging.Logger;
056:
057: /**
058: * JARV verifier implementation
059: */
060: public class VerifierHandlerImpl extends DefaultHandler implements
061: VerifierHandler {
062: private static final L10N L = new L10N(VerifierHandlerImpl.class);
063: protected static final Logger log = Logger
064: .getLogger(VerifierHandlerImpl.class.getName());
065:
066: // very verbose logging
067: private static final boolean _isDebug = false;
068:
069: private SchemaImpl _schema;
070: private VerifierImpl _verifier;
071: private boolean _hasProgram;
072:
073: private boolean _isValid = true;
074:
075: private LruCache<Object, Item> _programCache;
076:
077: private QName _name;
078: private ArrayList<QName> _nameStack = new ArrayList<QName>();
079: private String _eltLocation;
080: private ArrayList<String> _eltLocationStack = new ArrayList<String>();
081:
082: private Item _item;
083: private ArrayList<Item> _itemStack = new ArrayList<Item>();
084:
085: private Locator _locator;
086:
087: private boolean _isLogFinest;
088:
089: private CharBuffer _text = new CharBuffer();
090: private boolean _hasText;
091:
092: private StartKey _startKey = new StartKey();
093: private EndElementKey _endElementKey = new EndElementKey();
094:
095: /**
096: * Creates the Verifier Handler.
097: */
098: public VerifierHandlerImpl(SchemaImpl schema, VerifierImpl verifier) {
099: _schema = schema;
100: _programCache = _schema.getProgramCache();
101: _verifier = verifier;
102: }
103:
104: /**
105: * Sets the locator.
106: */
107: public void setDocumentLocator(Locator locator) {
108: _locator = locator;
109: }
110:
111: /**
112: * Sets the error handler
113: */
114: public void setErrorHandler(ErrorHandler errorHandler)
115: throws SAXException {
116: _verifier.setErrorHandler(errorHandler);
117: }
118:
119: private String getFileName() {
120: if (_locator != null)
121: return _locator.getSystemId();
122: else
123: return null;
124: }
125:
126: private int getLine() {
127: if (_locator != null)
128: return _locator.getLineNumber();
129: else
130: return -1;
131: }
132:
133: /**
134: * Called when the document starts.
135: */
136: public void startDocument() throws SAXException {
137: try {
138: _nameStack.clear();
139: _itemStack.clear();
140: _eltLocationStack.clear();
141:
142: _name = new QName("#top", "");
143: _item = _schema.getStartItem();
144:
145: _itemStack.add(_item);
146:
147: _eltLocation = getLocation();
148:
149: _isLogFinest = _isDebug && log.isLoggable(Level.FINEST);
150: _hasText = false;
151: _text.clear();
152: } catch (Exception e) {
153: error(e);
154: }
155: }
156:
157: /**
158: * Called when an element starts.
159: */
160: public void startElement(String uri, String localName,
161: String qName, Attributes attrs) throws SAXException {
162: if (!_isValid)
163: return;
164:
165: if (_hasText)
166: sendText();
167:
168: if (_isLogFinest)
169: log.finest("element start: " + qName);
170:
171: try {
172: QName parent = _name;
173: _nameStack.add(parent);
174:
175: String parentLocation = _eltLocation;
176: _eltLocationStack.add(parentLocation);
177:
178: QName name = new QName(qName, uri);
179: _name = name;
180: _eltLocation = getLocation();
181:
182: Item newItem = getStartElement(_item, name);
183:
184: if (newItem == null) {
185: Item parentItem = _itemStack.get(_itemStack.size() - 1);
186:
187: if (parent.getName().equals("#top"))
188: throw new RelaxException(L.l(
189: "<{0}> is an unexpected top-level tag.{1}",
190: errorNodeName(name, _item, parentItem),
191: errorMessageDetail(_item, parentItem, null,
192: name)));
193: else
194: throw new RelaxException(
195: L
196: .l(
197: "<{0}> is an unexpected tag (parent <{1}> starts at {2}).{3}",
198: errorNodeName(name, _item,
199: parentItem), parent
200: .getName(),
201: parentLocation,
202: errorMessageDetail(_item,
203: parentItem, parent
204: .getName(),
205: name)));
206: }
207:
208: _item = newItem;
209: _itemStack.add(newItem);
210:
211: Item parentItem = newItem;
212:
213: int len = attrs.getLength();
214: for (int i = 0; i < len; i++) {
215: String attrUri = attrs.getURI(i);
216: String attrQName = attrs.getQName(i);
217: String value = attrs.getValue(i);
218:
219: if (_isLogFinest)
220: log.finest("attribute: " + attrQName + "=\""
221: + value + "\"");
222:
223: name = new QName(attrQName, attrUri);
224:
225: if (attrQName.startsWith("xml:")) {
226: } else if (!_item.allowAttribute(name, value)) {
227: throw new RelaxException(
228: L
229: .l(
230: "{0}=\"{1}\" is an unexpected attribute in <{2}>.{3}",
231: attrQName, value, qName,
232: attributeMessageDetail(
233: _item, parentItem,
234: qName, null)));
235: } else
236: _item = _item.setAttribute(name, value);
237:
238: if (_item == null)
239: _item = EmptyItem.create();
240: }
241:
242: newItem = _item.attributeEnd();
243: if (newItem == null)
244: throw new RelaxException(L.l(
245: "<{0}> expects more attributes.{1}", qName,
246: attributeMessageDetail(_item, parentItem,
247: qName, null)));
248: _item = newItem;
249: } catch (Exception e) {
250: error(e);
251: }
252: }
253:
254: private Item getStartElement(Item item, QName name)
255: throws RelaxException {
256: _startKey.init(item, name);
257:
258: Item newItem = null;//_programCache.get(_startKey);
259:
260: if (newItem != null) {
261: return newItem;
262: }
263:
264: newItem = _item.startElement(name);
265:
266: /*
267: if (newItem != null)
268: _programCache.put(new StartKey(item, name), newItem);
269: */
270:
271: return newItem;
272: }
273:
274: public void characters(char ch[], int start, int length)
275: throws SAXException {
276: _hasText = true;
277: _text.append(ch, start, length);
278: }
279:
280: public void sendText() throws SAXException {
281: if (!_hasText)
282: return;
283:
284: _hasText = false;
285:
286: try {
287: Item newItem = _item.text(_text);
288:
289: if (newItem == null) {
290: String string = _text.toString();
291:
292: Item parentItem = _itemStack.get(_itemStack.size() - 1);
293:
294: throw new RelaxException(
295: L
296: .l(
297: "The following text is not allowed in this context.\n{0}\n{1}",
298: string, errorMessageDetail(
299: _item, parentItem,
300: _name.getName(), null)));
301: }
302:
303: _text.clear();
304: _item = newItem;
305: } catch (Exception e) {
306: _text.clear();
307: error(e);
308: }
309: }
310:
311: /**
312: * Called when an element ends.
313: */
314: public void endElement(String uri, String localName, String qName)
315: throws SAXException {
316: if (_hasText)
317: sendText();
318:
319: if (!_isValid)
320: return;
321:
322: if (_isLogFinest)
323: log.finest("element end: " + qName);
324:
325: QName name = _name;
326: QName parent = _nameStack.remove(_nameStack.size() - 1);
327: _name = parent;
328:
329: Item parentItem = _itemStack.remove(_itemStack.size() - 1);
330:
331: String eltOpen = _eltLocation;
332: _eltLocation = _eltLocationStack.remove(_eltLocationStack
333: .size() - 1);
334:
335: try {
336: Item nextItem = getEndElement(_item);
337:
338: if (nextItem == null)
339: throw new RelaxException(
340: L
341: .l(
342: "<{0}> closed while expecting more elements (open at {1}).{2}",
343: qName,
344: eltOpen,
345: requiredMessageDetail(_item,
346: parentItem, qName, null)));
347:
348: _item = nextItem;
349: } catch (Exception e) {
350: error(e);
351: }
352: }
353:
354: private Item getEndElement(Item item) throws RelaxException {
355: _endElementKey.init(item);
356:
357: Item newItem = null;//_programCache.get(_endElementKey);
358:
359: if (newItem != null) {
360: return newItem;
361: }
362:
363: newItem = _item.endElement();
364:
365: /*
366: if (newItem != null)
367: _programCache.put(new EndElementKey(item), newItem);
368: */
369:
370: return newItem;
371: }
372:
373: /**
374: * Called for errors.
375: */
376: private void error(SAXException e) throws SAXException {
377: _isValid = false;
378:
379: _verifier
380: .error(new SAXParseException(e.getMessage(), _locator));
381: }
382:
383: /**
384: * Called for errors.
385: */
386: private void error(Exception e) throws SAXException {
387: if (e instanceof RuntimeException)
388: throw (RuntimeException) e;
389: else if (e instanceof SAXException)
390: error((SAXException) e);
391: else
392: error(new SAXException(e.getMessage(), e));
393: }
394:
395: /**
396: * Returns a string containing the allowed values.
397: */
398: private String errorNodeName(QName name, Item item, Item parentItem) {
399: Item currentItem = item;
400:
401: if (currentItem == null)
402: currentItem = parentItem;
403:
404: if (currentItem == null)
405: return name.toString();
406:
407: HashSet<QName> values = new LinkedHashSet<QName>();
408: currentItem.firstSet(values);
409:
410: for (QName value : values) {
411: if (!name.getLocalName().equals(value.getLocalName())) {
412: } else if (name.getPrefix() == null
413: || name.getPrefix().equals("")) {
414: return name.getName() + " xmlns=\""
415: + name.getNamespaceURI() + "\"";
416: } else {
417: return name.getName() + " xmlns:" + name.getPrefix()
418: + "=\"" + name.getNamespaceURI() + "\"";
419: }
420: }
421:
422: return name.getName();
423: }
424:
425: /**
426: * Returns a string containing the allowed values.
427: */
428: private String errorMessageDetail(Item item, Item parentItem,
429: String parentName, QName qName) {
430: Item currentItem = item;
431:
432: if (currentItem == null)
433: currentItem = parentItem;
434:
435: HashSet<QName> values = new LinkedHashSet<QName>();
436: currentItem.firstSet(values);
437:
438: String expected = null;
439: if (values.size() <= 5)
440: expected = namesToString(values, parentName, qName,
441: currentItem.allowEmpty());
442:
443: return (getLineContext(getFileName(), getLine()) + syntaxMessage(
444: item, parentItem, parentName, qName, expected));
445: }
446:
447: /**
448: * Returns a string containing the allowed values.
449: */
450: private String requiredMessageDetail(Item item, Item parentItem,
451: String parentName, QName qName) {
452: Item currentItem = item;
453:
454: if (currentItem == null)
455: currentItem = parentItem;
456:
457: HashSet<QName> values = new LinkedHashSet<QName>();
458: currentItem.requiredFirstSet(values);
459:
460: String expected = null;
461:
462: if (values.size() <= 5) {
463: expected = namesToString(values, parentName, qName,
464: currentItem.allowEmpty());
465: }
466:
467: return (getLineContext(getFileName(), getLine()) + syntaxMessage(
468: item, parentItem, parentName, qName, expected));
469: }
470:
471: /**
472: * Returns a string containing the allowed values.
473: */
474: private String attributeMessageDetail(Item item, Item parentItem,
475: String parentName, QName qName) {
476: Item currentItem = item;
477:
478: if (currentItem == null)
479: currentItem = parentItem;
480:
481: String allowed = allowedAttributes(currentItem, qName);
482:
483: return (getLineContext(getFileName(), getLine()) + syntaxMessage(
484: item, parentItem, parentName, qName, allowed));
485: }
486:
487: /**
488: * Returns a string containing the allowed values.
489: */
490: private String syntaxMessage(Item item, Item parentItem,
491: String parentName, QName qName, String expected) {
492: String syntaxPrefix;
493:
494: if (parentName == null || parentName.equals("#top"))
495: syntaxPrefix = "Syntax: ";
496: else
497: syntaxPrefix = "<" + parentName + "> syntax: ";
498:
499: String msg = "";
500:
501: Item topParent = null;
502: for (Item parent = item; parent != null; parent = null) { // parent.getParent()) {
503: if (qName != null && parent.allowsElement(qName)) {
504: msg = "\n Check for duplicate and out-of-order tags.";
505:
506: if (expected != null)
507: msg += expected + "\n";
508:
509: msg += "\n";
510:
511: String prefix = "Syntax: ";
512: if (parent == parentItem)
513: prefix = syntaxPrefix;
514:
515: msg += prefix
516: + parent.toSyntaxDescription(prefix.length());
517: break;
518: }
519:
520: // topParent = parent;
521: }
522:
523: if (topParent == null || topParent instanceof EmptyItem) {
524: topParent = parentItem;
525:
526: if (qName != null && topParent.allowsElement(qName)) {
527: msg = "\n Check for duplicate and out-of-order tags.";
528:
529: if (expected != null)
530: msg += expected + "\n";
531:
532: msg += "\n";
533:
534: String prefix = syntaxPrefix;
535: msg += prefix
536: + topParent
537: .toSyntaxDescription(prefix.length());
538: }
539: }
540:
541: if (msg.equals("")) {
542: msg = "";
543:
544: if (expected != null)
545: msg += expected + "\n";
546:
547: msg += "\n";
548:
549: String prefix = syntaxPrefix;
550: msg += prefix
551: + topParent.toSyntaxDescription(prefix.length());
552: }
553:
554: return msg;
555: }
556:
557: /**
558: * Returns a string containing the allowed values.
559: */
560: private String requiredValues(Item item, String parentName,
561: QName qName) {
562: if (item == null)
563: return "";
564:
565: HashSet<QName> values = new LinkedHashSet<QName>();
566: item.requiredFirstSet(values);
567:
568: return namesToString(values, parentName, qName, item
569: .allowEmpty());
570: }
571:
572: private String namesToString(HashSet<QName> values,
573: String parentName, QName qName, boolean allowEmpty) {
574: CharBuffer cb = new CharBuffer();
575: if (values.size() > 0) {
576: ArrayList<QName> sortedValues = new ArrayList<QName>(values);
577: Collections.sort(sortedValues);
578:
579: for (int i = 0; i < sortedValues.size(); i++) {
580: QName name = sortedValues.get(i);
581:
582: if (i == 0)
583: cb.append("\n\n");
584: else if (i == sortedValues.size() - 1)
585: cb.append(" or\n");
586: else
587: cb.append(",\n");
588:
589: if (name.getName().equals("#text")) {
590: cb.append("text");
591: } else if (name.getNamespaceURI() == null
592: || qName == null)
593: cb.append("<" + name.getName() + ">");
594: else if (qName.getNamespaceURI() != name
595: .getNamespaceURI()) {
596: if (name.getPrefix() != null)
597: cb.append("<" + name.getName() + " xmlns:"
598: + name.getPrefix() + "=\""
599: + name.getNamespaceURI() + "\">");
600: else
601: cb.append("<" + name.getName() + " xmlns=\""
602: + name.getNamespaceURI() + "\">");
603: } else
604: cb.append("<" + name.getName() + ">");
605: }
606:
607: if (values.size() == 1)
608: cb.append(" is expected");
609: else
610: cb.append(" are expected");
611:
612: if (allowEmpty) {
613: if (parentName == null || parentName.equals("#top"))
614: cb.append(",\nor the document may end.");
615: else
616: cb.append(",\nor </" + parentName + "> may close.");
617: } else
618: cb.append(".");
619: } else if (allowEmpty) {
620: if (parentName == null || parentName.equals("#top"))
621: cb.append("\n\nThe document is expected to end.");
622: else
623: cb.append("\n\n</" + parentName
624: + "> is expected to close.");
625: }
626:
627: return cb.toString();
628: }
629:
630: /**
631: * Returns a string containing the allowed values.
632: */
633: private String allowedAttributes(Item item, QName qName) {
634: if (item == null)
635: return "";
636:
637: HashSet<QName> values = new LinkedHashSet<QName>();
638: item.attributeSet(values);
639:
640: CharBuffer cb = new CharBuffer();
641: if (values.size() > 0) {
642: ArrayList<QName> sortedValues = new ArrayList<QName>(values);
643: Collections.sort(sortedValues);
644:
645: for (int i = 0; i < sortedValues.size(); i++) {
646: QName name = sortedValues.get(i);
647:
648: if (i == 0)
649: cb.append("\n\n");
650: else if (i == sortedValues.size() - 1)
651: cb.append(" or ");
652: else
653: cb.append(", ");
654:
655: String uri = name.getNamespaceURI();
656: if (uri == null || uri.equals(""))
657: cb.append("'" + name.getName() + "'");
658: else if (qName == null
659: || qName.getName().equals(name.getName()))
660: cb.append("'" + name.getCanonicalName() + "'");
661: else
662: cb.append("'" + name.getName() + "'");
663: }
664:
665: if (values.size() == 1)
666: cb.append(" is expected.");
667: else
668: cb.append(" are expected.");
669: }
670:
671: return cb.toString();
672: }
673:
674: /**
675: * Returns the current location.
676: */
677: private String getLocation() {
678: if (_locator == null)
679: return "";
680: else
681: return "" + _locator.getLineNumber();
682: }
683:
684: /**
685: * Checks if the document was valid.
686: *
687: * <p>
688: * This method can be only called after this handler receives
689: * the <code>endDocument</code> event.
690: *
691: * @return
692: * <b>true</b> if the document was valid,
693: * <b>false</b> if not.
694: *
695: * @exception IllegalStateException
696: * If this method is called before the endDocument event is dispatched.
697: */
698: public boolean isValid() throws IllegalStateException {
699: return _isValid;
700: }
701:
702: private String getLineContext(String filename, int errorLine) {
703: if (filename == null || errorLine <= 0)
704: return "";
705:
706: ReadStream is = null;
707: try {
708: Path path = Vfs.lookup().lookup(filename);
709:
710: StringBuilder sb = new StringBuilder("\n\n");
711:
712: is = path.openRead();
713: int line = 0;
714: String text;
715: while ((text = is.readLine()) != null) {
716: line++;
717:
718: if (errorLine - 2 <= line && line <= errorLine + 2) {
719: sb.append(line);
720: sb.append(": ");
721: sb.append(text);
722: sb.append("\n");
723: }
724: }
725:
726: return sb.toString();
727: } catch (IOException e) {
728: log.log(Level.FINEST, e.toString(), e);
729:
730: return "";
731: } finally {
732: if (is != null)
733: is.close();
734: }
735: }
736:
737: static class StartKey {
738: private Item _item;
739: private QName _name;
740:
741: public StartKey(Item item, QName name) {
742: _item = item;
743: _name = name;
744: }
745:
746: public StartKey() {
747: }
748:
749: public void init(Item item, QName name) {
750: _item = item;
751: _name = name;
752: }
753:
754: public int hashCode() {
755: return _name.hashCode() + 137
756: * System.identityHashCode(_item);
757: }
758:
759: public boolean equals(Object o) {
760: if (o == this )
761: return true;
762:
763: if (o.getClass() != StartKey.class)
764: return false;
765:
766: StartKey key = (StartKey) o;
767:
768: return _name.equals(key._name) && _item == key._item;
769: }
770: }
771:
772: static class EndElementKey {
773: private Item _item;
774:
775: public EndElementKey(Item item) {
776: _item = item;
777: }
778:
779: public EndElementKey() {
780: }
781:
782: public void init(Item item) {
783: _item = item;
784: }
785:
786: public int hashCode() {
787: return 137 + _item.hashCode();
788: }
789:
790: public boolean equals(Object o) {
791: if (o == this )
792: return true;
793:
794: if (o.getClass() != EndElementKey.class)
795: return false;
796:
797: EndElementKey key = (EndElementKey) o;
798:
799: return _item.equals(key._item);
800: }
801: }
802: }
|