001 /*
002 * (c) Copyright 2009 University of Bristol
003 * All rights reserved.
004 * [See end of file]
005 */
006 package net.rootdev.javardfa;
007
008 import com.hp.hpl.jena.iri.IRI;
009 import com.hp.hpl.jena.iri.IRIFactory;
010 import java.io.IOException;
011 import java.io.StringWriter;
012 import java.util.Arrays;
013 import java.util.Collection;
014 import java.util.Collections;
015 import java.util.EnumSet;
016 import java.util.HashMap;
017 import java.util.HashSet;
018 import java.util.Iterator;
019 import java.util.LinkedList;
020 import java.util.List;
021 import java.util.Map;
022 import java.util.Set;
023 import javax.xml.namespace.NamespaceContext;
024 import javax.xml.namespace.QName;
025 import javax.xml.stream.XMLEventFactory;
026 import javax.xml.stream.XMLEventReader;
027 import javax.xml.stream.XMLEventWriter;
028 import javax.xml.stream.XMLOutputFactory;
029 import javax.xml.stream.XMLStreamException;
030 import javax.xml.stream.events.Attribute;
031 import javax.xml.stream.events.StartElement;
032 import javax.xml.stream.events.XMLEvent;
033 import org.xml.sax.Attributes;
034 import org.xml.sax.ContentHandler;
035 import org.xml.sax.Locator;
036 import org.xml.sax.SAXException;
037
038 /**
039 * @author Damian Steer <pldms@mac.com>
040 */
041 public class Parser implements ContentHandler {
042
043 final static List<String> _allowed = Arrays.asList(
044 "alternate", "appendix", "bookmark", "cite",
045 "chapter", "contents", "copyright", "first",
046 "glossary", "help", "icon", "index", "last",
047 "license", "meta", "next", "p3pv1", "prev",
048 "collection", "role", "section", "stylesheet",
049 "subsection", "start", "top", "up");
050 final static Set<String> SpecialRels = new HashSet<String>(_allowed);
051 final static IRIFactory IRIFact = IRIFactory.semanticWebImplementation();
052 final static XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
053 final static XMLEventFactory EventFactory = XMLEventFactory.newInstance();
054 private final XMLEventReader reader;
055 private final StatementSink sink;
056 // Suggestion: switch this for object produced by factory that matches QNames
057 // we can then en-slacken if needed by passing in different factory etc
058 final QName about = new QName("about"); // safe
059 final QName resource = new QName("resource"); // safe
060 final QName href = new QName("href"); // URI
061 final QName src = new QName("src"); // URI
062 final QName property = new QName("property"); // CURIE
063 final QName datatype = new QName("datatype"); // CURIE
064 final QName typeof = new QName("typeof"); // CURIE
065 final QName rel = new QName("rel"); // Link types and CURIES
066 final QName rev = new QName("rev"); // Link type and CURIES
067 final QName content = new QName("content");
068 final QName xmllang = new QName("http://www.w3.org/XML/1998/namespace", "lang", "xml");
069 final QName lang = new QName("lang");
070 final QName fakeXmlLang = new QName("xml:lang");
071 final QName base = new QName("http://www.w3.org/1999/xhtml", "base");
072 final QName head = new QName("http://www.w3.org/1999/xhtml", "head");
073 final QName body = new QName("http://www.w3.org/1999/xhtml", "body");
074 // Hack bits
075 final QName input = new QName("input");
076 final QName name = new QName("name");
077 final QName form = new QName("form");
078 final Collection<String> rdfType = Collections.singleton("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
079 final String xmlLiteral = "http://www.w3.org/1999/02/22-rdf-syntax-ns#XMLLiteral";
080 final Set<Setting> settings = EnumSet.noneOf(Setting.class);
081
082 public enum Setting {
083
084 FormMode, ManualNamespaces
085 }
086
087 public Parser(StatementSink sink) {
088 this.reader = null;
089 this.sink = sink;
090 outputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
091 }
092
093 public void enable(Setting setting) {
094 settings.add(setting);
095 }
096
097 public void disable(Setting setting) {
098 settings.remove(setting);
099 }
100
101 public void setBase(String base) {
102 this.context = new EvalContext(base);
103 }
104
105 EvalContext parse(EvalContext context, StartElement element) throws XMLStreamException {
106 boolean recurse = true;
107 boolean skipElement = false;
108 String newSubject = null;
109 String currentObject = null;
110 List<String> forwardProperties = new LinkedList(context.forwardProperties);
111 List<String> backwardProperties = new LinkedList(context.backwardProperties);
112 String currentLanguage = context.language;
113 boolean langIsLang = context.langIsLang;
114
115 if (element.getAttributeByName(xmllang) != null) {
116 currentLanguage = element.getAttributeByName(xmllang).getValue();
117 }
118
119 if (settings.contains(Setting.ManualNamespaces) &&
120 element.getAttributeByName(fakeXmlLang) != null &&
121 !langIsLang) {
122 currentLanguage = element.getAttributeByName(fakeXmlLang).getValue();
123 }
124
125 if (settings.contains(Setting.ManualNamespaces) &&
126 element.getAttributeByName(lang) != null) {
127 langIsLang = true;
128 currentLanguage = element.getAttributeByName(lang).getValue();
129 }
130
131 if (base.equals(element.getName()) &&
132 element.getAttributeByName(href) != null) {
133 context.setBase(element.getAttributeByName(href).getValue());
134 }
135
136 if (element.getAttributeByName(rev) == null &&
137 element.getAttributeByName(rel) == null) {
138 Attribute nSubj = findAttribute(element, about, src, resource, href);
139 if (nSubj != null) {
140 newSubject = getURI(context.base, element, nSubj);
141 } else {
142 if (element.getAttributeByName(typeof) != null) {
143 if (body.equals(element.getName()) ||
144 head.equals(element.getName())) {
145 newSubject = context.base;
146 } else {
147 newSubject = createBNode();
148 }
149 } else {
150 if (context.parentObject != null) {
151 newSubject = context.parentObject;
152 }
153 if (element.getAttributeByName(property) == null) {
154 skipElement = true;
155 }
156 }
157 }
158 } else {
159 Attribute nSubj = findAttribute(element, about, src);
160 if (nSubj != null) {
161 newSubject = getURI(context.base, element, nSubj);
162 } else {
163 // TODO if element is head or body assume about=""
164 if (element.getAttributeByName(typeof) != null) {
165 newSubject = createBNode();
166 } else {
167 if (context.parentObject != null) {
168 newSubject = context.parentObject;
169 }
170 }
171 }
172 Attribute cObj = findAttribute(element, resource, href);
173 if (cObj != null) {
174 currentObject = getURI(context.base, element, cObj);
175 }
176 }
177
178 if (newSubject != null && element.getAttributeByName(typeof) != null) {
179 List<String> types = getURIs(context.base, element, element.getAttributeByName(typeof));
180 for (String type : types) {
181 emitTriples(newSubject,
182 rdfType,
183 type);
184 }
185 }
186
187 if (newSubject == null) {
188 newSubject = context.parentSubject;
189 }
190
191 // Dodgy extension
192 if (settings.contains(Setting.FormMode)) {
193 if (form.equals(element.getName())) {
194 emitTriples(newSubject, rdfType, "http://www.w3.org/1999/xhtml/vocab/#form"); // Signal entering form
195 }
196 if (input.equals(element.getName()) &&
197 element.getAttributeByName(name) != null) {
198 currentObject = "?" + element.getAttributeByName(name).getValue();
199 }
200
201 }
202
203 if (currentObject != null) {
204 if (element.getAttributeByName(rel) != null) {
205 emitTriples(newSubject,
206 getURIs(context.base, element, element.getAttributeByName(rel)),
207 currentObject);
208 }
209 if (element.getAttributeByName(rev) != null) {
210 emitTriples(currentObject,
211 getURIs(context.base, element, element.getAttributeByName(rev)),
212 newSubject);
213 }
214 } else {
215 if (element.getAttributeByName(rel) != null) {
216 forwardProperties.addAll(getURIs(context.base, element, element.getAttributeByName(rel)));
217 }
218 if (element.getAttributeByName(rev) != null) {
219 backwardProperties.addAll(getURIs(context.base, element, element.getAttributeByName(rev)));
220 }
221 if (element.getAttributeByName(rel) != null || // if predicate present
222 element.getAttributeByName(rev) != null) {
223 currentObject = createBNode();
224 }
225 }
226
227 // Getting literal values. Complicated!
228
229 if (element.getAttributeByName(property) != null) {
230 List<String> props = getURIs(context.base, element, element.getAttributeByName(property));
231 String dt = getDatatype(element);
232 if (element.getAttributeByName(content) != null) { // The easy bit
233 String lex = element.getAttributeByName(content).getValue();
234 if (dt == null || dt.length() == 0) {
235 emitTriplesPlainLiteral(newSubject, props, lex, currentLanguage);
236 } else {
237 emitTriplesDatatypeLiteral(newSubject, props, lex, dt);
238 }
239 } else {
240 //recurse = false;
241 level = 1;
242 theDatatype = dt;
243 literalWriter = new StringWriter();
244 litProps = props;
245 if (dt == null) // either plain or xml. defer decision
246 {
247 queuedEvents = new LinkedList<XMLEvent>();
248 } else if (xmlLiteral.equals(dt)) // definitely xml?
249 {
250 xmlWriter = outputFactory.createXMLEventWriter(literalWriter);
251 }
252
253 }
254 }
255
256 if (!skipElement && newSubject != null) {
257 emitTriples(context.parentSubject,
258 context.forwardProperties,
259 newSubject);
260
261 emitTriples(newSubject,
262 context.backwardProperties,
263 context.parentSubject);
264 }
265
266 if (recurse) {
267 EvalContext ec = new EvalContext(context);
268
269 if (skipElement) {
270 ec.language = currentLanguage;
271 ec.langIsLang = langIsLang;
272 ec.original = context.original;
273 } else {
274 if (newSubject != null) {
275 ec.parentSubject = newSubject;
276 } else {
277 ec.parentSubject = context.parentSubject;
278 }
279
280 if (currentObject != null) {
281 ec.parentObject = currentObject;
282 } else if (newSubject != null) {
283 ec.parentObject = newSubject;
284 } else {
285 ec.parentObject = context.parentSubject;
286 }
287
288 ec.language = currentLanguage;
289 ec.langIsLang = langIsLang;
290 ec.forwardProperties = forwardProperties;
291 ec.backwardProperties = backwardProperties;
292 }
293
294 return ec;
295 }
296
297 return null;
298 }
299
300 private Attribute findAttribute(StartElement element, QName... names) {
301 for (QName aName : names) {
302 Attribute a = element.getAttributeByName(aName);
303 if (a != null) {
304 return a;
305 }
306 }
307 return null;
308 }
309
310 private void emitTriples(String subj, Collection<String> props, String obj) {
311 for (String prop : props) {
312 sink.addObject(subj, prop, obj);
313 }
314 }
315
316 private void emitTriplesPlainLiteral(String subj, Collection<String> props, String lex, String language) {
317 for (String prop : props) {
318 sink.addLiteral(subj, prop, lex, language, null);
319 }
320 }
321
322 private void emitTriplesDatatypeLiteral(String subj, Collection<String> props, String lex, String datatype) {
323 for (String prop : props) {
324 sink.addLiteral(subj, prop, lex, null, datatype);
325 }
326 }
327
328 private String getURI(String base, StartElement element, Attribute attr) {
329 QName attrName = attr.getName();
330 if (attrName.equals(href) || attrName.equals(src)) // A URI
331 {
332 if (attr.getValue().length() == 0) {
333 return base;
334 }
335 IRI uri = IRIFact.construct(base);
336 IRI resolved = uri.resolve(attr.getValue());
337 return resolved.toString();
338 }
339 if (attrName.equals(about) || attrName.equals(resource)) // Safe CURIE or URI
340 {
341 return expandSafeCURIE(base, element, attr.getValue());
342 }
343 if (attrName.equals(datatype)) // A CURIE
344 {
345 return expandCURIE(element, attr.getValue());
346 }
347 throw new RuntimeException("Unexpected attribute: " + attr);
348 }
349
350 private List<String> getURIs(String base, StartElement element, Attribute attr) {
351 List<String> uris = new LinkedList<String>();
352 String[] curies = attr.getValue().split("\\s+");
353 boolean permitReserved = rel.equals(attr.getName()) ||
354 rev.equals(attr.getName());
355 for (String curie : curies) {
356 boolean isSpecial = (settings.contains(Setting.ManualNamespaces)) ? SpecialRels.contains(curie.toLowerCase()) : SpecialRels.contains(curie);
357 if (isSpecial && settings.contains(Setting.ManualNamespaces)) {
358 curie = curie.toLowerCase();
359 }
360 if (permitReserved && isSpecial) {
361 uris.add("http://www.w3.org/1999/xhtml/vocab#" + curie);
362 } else if (!isSpecial) {
363 String uri = expandCURIE(element, curie);
364 if (uri != null) {
365 uris.add(uri);
366 }
367 }
368 }
369 return uris;
370 }
371 int bnodeId = 0;
372
373 private String createBNode() // TODO probably broken? Can you write bnodes in rdfa directly?
374 {
375 return "_:node" + (bnodeId++);
376 }
377
378 private String expandCURIE(StartElement element, String value) {
379 if (value.startsWith("_:") && element.getNamespaceURI("_") == null) {
380 return value;
381 }
382 if (settings.contains(Setting.FormMode) && // variable
383 value.startsWith("?")) {
384 return value;
385 }
386 int offset = value.indexOf(":") + 1;
387 if (offset == 0) {
388 //throw new RuntimeException("Is this a curie? \"" + value + "\"");
389 return null;
390 }
391 String prefix = value.substring(0, offset - 1);
392 String namespaceURI = prefix.length() == 0 ? "http://www.w3.org/1999/xhtml/vocab#" : element.getNamespaceURI(prefix);
393 if (namespaceURI == null) {
394 return null;
395 //throw new RuntimeException("Unknown prefix: " + prefix);
396 }
397 if (offset != value.length() && value.charAt(offset) == '#') {
398 offset += 1; // ex:#bar
399 }
400 if (namespaceURI.endsWith("/") || namespaceURI.endsWith("#")) {
401 return namespaceURI + value.substring(offset);
402 } else {
403 return namespaceURI + "#" + value.substring(offset);
404 }
405 }
406
407 private String expandSafeCURIE(String base, StartElement element, String value) {
408 if (value.startsWith("[") && value.endsWith("]")) {
409 return expandCURIE(element, value.substring(1, value.length() - 1));
410 } else {
411 if (value.length() == 0) {
412 return base;
413 }
414
415 if (settings.contains(Setting.FormMode) &&
416 value.startsWith("?")) {
417 return value;
418 }
419
420 IRI uri = IRIFact.construct(base);
421 IRI resolved = uri.resolve(value);
422 return resolved.toString();
423 }
424 }
425
426 private String getDatatype(StartElement element) {
427 Attribute de = element.getAttributeByName(datatype);
428 if (de == null) {
429 return null;
430 }
431 String dt = de.getValue();
432 if (dt.length() == 0) {
433 return dt;
434 }
435 return expandCURIE(element, dt);
436 }
437
438 static class EvalContext implements NamespaceContext {
439
440 EvalContext parent;
441 String base;
442 String parentSubject;
443 String parentObject;
444 String language;
445 List<String> forwardProperties;
446 List<String> backwardProperties;
447 Map<String, String> prefixToUri = new HashMap<String, String>();
448 boolean original;
449 boolean langIsLang = false; // html 5 oddity
450
451 private EvalContext(String base) {
452 this.base = base;
453 this.parentSubject = base;
454 this.forwardProperties = new LinkedList<String>();
455 this.backwardProperties = new LinkedList<String>();
456 original = true;
457 }
458
459 public EvalContext(EvalContext toCopy) {
460 this.base = toCopy.base;
461 this.parentSubject = toCopy.parentSubject;
462 this.parentObject = toCopy.parentObject;
463 this.language = toCopy.language;
464 this.forwardProperties = new LinkedList<String>(toCopy.forwardProperties);
465 this.backwardProperties = new LinkedList<String>(toCopy.backwardProperties);
466 this.langIsLang = toCopy.langIsLang;
467 this.original = false;
468 this.parent = toCopy;
469 }
470
471 public void setBase(String abase) {
472 if (abase.contains("#")) {
473 this.base = abase.substring(0, abase.indexOf("#"));
474 } else {
475 this.base = abase;
476 }
477 // Not great, but passes tests.
478 // We want to say: if parentSubject hasn't been changed, it's base
479 if (this.original) {
480 this.parentSubject = this.base;
481 }
482 if (parent != null) {
483 parent.setBase(base);
484 }
485 }
486
487 public void setNamespaceURI(String prefix, String uri) {
488 if (uri.length() == 0) {
489 uri = base;
490 }
491 prefixToUri.put(prefix, uri);
492 }
493
494 public String getNamespaceURI(String prefix) {
495 if (prefixToUri.containsKey(prefix)) {
496 return prefixToUri.get(prefix);
497 } else if (parent != null) {
498 return parent.getNamespaceURI(prefix);
499 } else {
500 return null;
501 }
502 }
503
504 public String getPrefix(String uri) {
505 throw new UnsupportedOperationException("Not supported yet.");
506 }
507
508 public Iterator getPrefixes(String uri) {
509 throw new UnsupportedOperationException("Not supported yet.");
510 }
511 }
512
513 private void getNamespaces(Attributes attrs) {
514 for (int i = 0; i < attrs.getLength(); i++) {
515 String qname = attrs.getQName(i);
516 String prefix = getPrefix(qname);
517 if ("xmlns".equals(prefix)) {
518 context.setNamespaceURI(getLocal(prefix, qname), attrs.getValue(i));
519 }
520 }
521 }
522
523 private String getPrefix(String qname) {
524 if (!qname.contains(":")) {
525 return "";
526 }
527 return qname.substring(0, qname.indexOf(":"));
528 }
529
530 private String getLocal(String prefix, String qname) {
531 if (prefix.length() == 0) {
532 return qname;
533 }
534 return qname.substring(prefix.length() + 1);
535 }
536 /**
537 * SAX methods
538 */
539 private Locator locator;
540 //private NSMapping mapping;
541 private EvalContext context = new EvalContext("http://www.example.com/");
542 // For literals (what fun!)
543 private List<XMLEvent> queuedEvents;
544 private int level = -1;
545 private XMLEventWriter xmlWriter;
546 private StringWriter literalWriter;
547 private String theDatatype;
548 private List<String> litProps;
549
550 public void setDocumentLocator(Locator arg0) {
551 this.locator = arg0;
552 }
553
554 public void startDocument() throws SAXException {
555 sink.start();
556 }
557
558 public void endDocument() throws SAXException {
559 sink.end();
560 }
561
562 public void startPrefixMapping(String arg0, String arg1)
563 throws SAXException {
564 context.setNamespaceURI(arg0, arg1);
565 }
566
567 public void endPrefixMapping(String arg0) throws SAXException {
568 }
569
570 public void startElement(String arg0, String localname, String qname, Attributes arg3) throws SAXException {
571 try {
572 //System.err.println("Start element: " + arg0 + " " + arg1 + " " + arg2);
573 // Dammit, not quite the same as XMLEventFactory
574 String prefix = (localname.equals(qname)) ? ""
575 : qname.substring(0, qname.indexOf(':'));
576 if (settings.contains(Setting.ManualNamespaces)) {
577 getNamespaces(arg3);
578 }
579 StartElement e = EventFactory.createStartElement(
580 prefix, arg0, localname,
581 fromAttributes(arg3), null, context);
582
583 if (level != -1) { // getting literal
584 handleForLiteral(e);
585 return;
586 }
587 context = parse(context, e);
588 } catch (XMLStreamException ex) {
589 throw new RuntimeException("Streaming issue", ex);
590 }
591
592 }
593
594 public void endElement(String arg0, String localname, String qname) throws SAXException {
595 //System.err.println("End element: " + arg0 + " " + arg1 + " " + arg2);
596 if (level != -1) { // getting literal
597 String prefix = (localname.equals(qname)) ? ""
598 : qname.substring(0, qname.indexOf(':'));
599 XMLEvent e = EventFactory.createEndElement(prefix, arg0, localname);
600 handleForLiteral(e);
601 if (level != -1) {
602 return; // if still handling literal duck out now
603 }
604 }
605 context = context.parent;
606 }
607
608 public void characters(char[] arg0, int arg1, int arg2) throws SAXException {
609 if (level != -1) {
610 XMLEvent e = EventFactory.createCharacters(String.valueOf(arg0, arg1, arg2));
611 handleForLiteral(e);
612 return;
613 }
614 }
615
616 public void ignorableWhitespace(char[] arg0, int arg1, int arg2) throws SAXException {
617 //System.err.println("Whitespace...");
618 if (level != -1) {
619 XMLEvent e = EventFactory.createIgnorableSpace(String.valueOf(arg0, arg1, arg2));
620 handleForLiteral(e);
621 }
622 }
623
624 public void processingInstruction(String arg0, String arg1) throws SAXException {
625 }
626
627 public void skippedEntity(String arg0) throws SAXException {
628 }
629
630 private Iterator fromAttributes(Attributes attributes) {
631 List toReturn = new LinkedList();
632 boolean haveLang = false;
633 for (int i = 0; i < attributes.getLength(); i++) {
634 String qname = attributes.getQName(i);
635 String prefix = qname.contains(":") ? qname.substring(0, qname.indexOf(":")) : "";
636 Attribute attr = EventFactory.createAttribute(
637 prefix, attributes.getURI(i),
638 attributes.getLocalName(i), attributes.getValue(i));
639 if (xmllang.getLocalPart().equals(attributes.getLocalName(i)) &&
640 xmllang.getNamespaceURI().equals(attributes.getURI(i))) {
641 haveLang = true;
642 }
643 toReturn.add(attr);
644 }
645 // Copy xml lang across if in literal
646 if (level == 1 && context.language != null && !haveLang) {
647 toReturn.add(EventFactory.createAttribute(xmllang, context.language));
648 }
649 return toReturn.iterator();
650 }
651
652 private void handleForLiteral(XMLEvent e) {
653 try {
654 handleForLiteralEx(e);
655 } catch (XMLStreamException ex) {
656 throw new RuntimeException("Literal handling error", ex);
657 } catch (IOException ex) {
658 throw new RuntimeException("Literal handling error", ex);
659 }
660 }
661
662 private void handleForLiteralEx(XMLEvent e) throws XMLStreamException, IOException {
663 if (e.isStartElement()) {
664 level++;
665 if (queuedEvents != null) { // Aha, we ain't plain
666 xmlWriter = outputFactory.createXMLEventWriter(literalWriter);
667 for (XMLEvent ev : queuedEvents) {
668 xmlWriter.add(ev);
669 }
670 queuedEvents = null;
671 theDatatype = xmlLiteral;
672 }
673 }
674
675 if (e.isEndElement()) {
676 level--;
677 if (level == 0) { // Finished!
678 if (xmlWriter != null) {
679 xmlWriter.close();
680 } else if (queuedEvents != null) {
681 for (XMLEvent ev : queuedEvents) {
682 literalWriter.append(ev.asCharacters().getData());
683 }
684 }
685 String lex = literalWriter.toString();
686 if (theDatatype == null || theDatatype.length() == 0) {
687 emitTriplesPlainLiteral(context.parentSubject,
688 litProps, lex, context.language);
689 } else {
690 emitTriplesDatatypeLiteral(context.parentSubject,
691 litProps, lex, theDatatype);
692 }
693 queuedEvents = null;
694 xmlWriter = null;
695 literalWriter = null;
696 theDatatype = null;
697 litProps = null;
698 level = -1;
699 return;
700 }
701 }
702
703 if (xmlWriter != null) {
704 xmlWriter.add(e);
705 } else if (e.isCharacters() && queuedEvents != null) {
706 queuedEvents.add(e);
707 } else if (e.isCharacters()) {
708 literalWriter.append(e.asCharacters().getData());
709 }
710 }
711 }
712
713 /*
714 * (c) Copyright 2009 University of Bristol
715 * All rights reserved.
716 *
717 * Redistribution and use in source and binary forms, with or without
718 * modification, are permitted provided that the following conditions
719 * are met:
720 * 1. Redistributions of source code must retain the above copyright
721 * notice, this list of conditions and the following disclaimer.
722 * 2. Redistributions in binary form must reproduce the above copyright
723 * notice, this list of conditions and the following disclaimer in the
724 * documentation and/or other materials provided with the distribution.
725 * 3. The name of the author may not be used to endorse or promote products
726 * derived from this software without specific prior written permission.
727 *
728 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
729 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
730 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
731 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
732 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
733 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
734 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
735 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
736 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
737 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
738 */