Source code: org/apache/derby/iapi/types/XML.java
1 /*
2
3 Derby - Class org.apache.derby.iapi.types.XML
4
5 Copyright 2005 The Apache Software Foundation or its licensors, as applicable.
6
7 Licensed under the Apache License, Version 2.0 (the "License");
8 you may not use this file except in compliance with the License.
9 You may obtain a copy of the License at
10
11 http://www.apache.org/licenses/LICENSE-2.0
12
13 Unless required by applicable law or agreed to in writing, software
14 distributed under the License is distributed on an "AS IS" BASIS,
15 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 See the License for the specific language governing permissions and
17 limitations under the License.
18
19 */
20
21 package org.apache.derby.iapi.types;
22
23 import org.apache.derby.iapi.error.StandardException;
24
25 import org.apache.derby.iapi.services.cache.ClassSize;
26 import org.apache.derby.iapi.services.io.ArrayInputStream;
27 import org.apache.derby.iapi.services.io.StoredFormatIds;
28 import org.apache.derby.iapi.services.io.StreamStorable;
29 import org.apache.derby.iapi.services.sanity.SanityManager;
30
31 import org.apache.derby.iapi.types.DataValueDescriptor;
32 import org.apache.derby.iapi.types.StringDataValue;
33 import org.apache.derby.iapi.types.BooleanDataValue;
34
35 import org.apache.derby.iapi.reference.SQLState;
36
37 import java.sql.ResultSet;
38 import java.sql.SQLException;
39 import java.sql.Types;
40
41 import java.io.InputStream;
42 import java.io.IOException;
43 import java.io.ObjectOutput;
44 import java.io.ObjectInput;
45 import java.io.StringReader;
46
47 import org.xml.sax.ErrorHandler;
48 import org.xml.sax.XMLReader;
49 import org.xml.sax.SAXException;
50 import org.xml.sax.SAXParseException;
51 import org.xml.sax.InputSource;
52
53 import org.xml.sax.helpers.DefaultHandler;
54 import org.xml.sax.helpers.XMLReaderFactory;
55
56 import javax.xml.transform.Templates;
57 import javax.xml.transform.TransformerFactory;
58
59 import javax.xml.transform.sax.SAXResult;
60 import javax.xml.transform.sax.TemplatesHandler;
61 import javax.xml.transform.sax.TransformerHandler;
62
63 // Note that even though the following has a Xalan
64 // package name, it IS part of the JDK 1.4 API, and
65 // thus we can compile it without having Xalan in
66 // our classpath.
67 import org.apache.xalan.processor.TransformerFactoryImpl;
68
69 /**
70 * This type implements the XMLDataValue interface and thus is
71 * the type on which all XML related operations are executed.
72 *
73 * The first and simplest XML store implementation is a UTF-8
74 * based one--all XML data is stored on disk as a UTF-8 string,
75 * just like the other Derby string types. In order to make
76 * it possible for smarter XML implementations to exist in
77 * the future, this class always writes an "XML implementation
78 * id" to disk before writing the rest of its data. When
79 * reading the data, the impl id is read first and serves
80 * as an indicator of how the rest of the data should be
81 * read.
82 *
83 * So long as there's only one implementation (UTF-8)
84 * the impl id can be ignored; but when smarter implementations
85 * are written, the impl id will be the key to figuring out
86 * how an XML value should be read, written, and processed.
87 */
88 public class XML
89 extends DataType implements XMLDataValue, StreamStorable
90 {
91 // Id for this implementation. Should be unique
92 // across all XML type implementations.
93 protected static final short UTF8_IMPL_ID = 0;
94
95 // Parser class to use for parsing XML. We use the
96 // Xerces parser, so (for now) we require that Xerces
97 // be in the user's classpath. Note that we load
98 // the Xerces class dynamically (using the class
99 // name) so that Derby will build even if Xerces
100 // isn't in the build environment; i.e. Xerces is
101 // only required if XML is actually going to be used
102 // at runtime; it's not required for a successful
103 // build nor for non-XML database use.
104 protected static final String XML_PARSER_CLASS =
105 "org.apache.xerces.parsers.SAXParser";
106
107 // Guess at how much memory this type will take.
108 private static final int BASE_MEMORY_USAGE =
109 ClassSize.estimateBaseFromCatalog(XML.class);
110
111 // The actual XML data in this implementation is just a simple
112 // string, so this class really just wraps a SQLChar and
113 // defers most calls to the corresponding calls on that
114 // SQLChar. Note that, even though a SQLChar is the
115 // underlying implementation, an XML value is nonetheless
116 // NOT considered comparable nor compatible with any of
117 // Derby string types.
118 private SQLChar xmlStringValue;
119
120 // An XML reader for reading and parsing SAX events.
121 protected XMLReader saxReader;
122
123 // XSLT objects used when performing an XSLT query, which
124 // is the query mechanism for this UTF8-based implementation.
125 private static final String XPATH_PLACEHOLDER = "XPATH_PLACEHOLDER";
126 private static final String QUERY_MATCH_STRING = "MATCH";
127 private static String xsltStylesheet;
128 private XMLReader xsltReader;
129 private TransformerFactoryImpl saxTFactory;
130
131 /**
132 * Default constructor.
133 */
134 public XML()
135 {
136 xmlStringValue = null;
137 }
138
139 /**
140 * Private constructor used for the getClone() method.
141 * Takes a SQLChar and clones it.
142 * @param val A SQLChar instance to clone and use for
143 * this XML value.
144 */
145 private XML(SQLChar val)
146 {
147 xmlStringValue = (val == null ? null : (SQLChar)val.getClone());
148 }
149
150 /* ****
151 * DataValueDescriptor interface.
152 * */
153
154 /**
155 * @see DataValueDescriptor#getClone
156 */
157 public DataValueDescriptor getClone()
158 {
159 return new XML(xmlStringValue);
160 }
161
162 /**
163 * @see DataValueDescriptor#getNewNull
164 */
165 public DataValueDescriptor getNewNull()
166 {
167 return new XML();
168 }
169
170 /**
171 * @see DataValueDescriptor#getTypeName
172 */
173 public String getTypeName()
174 {
175 return TypeId.XML_NAME;
176 }
177
178 /**
179 * @see DataValueDescriptor#typePrecedence
180 */
181 public int typePrecedence()
182 {
183 return TypeId.XML_PRECEDENCE;
184 }
185
186 /**
187 * @see DataValueDescriptor#getString
188 */
189 public String getString() throws StandardException
190 {
191 return (xmlStringValue == null) ? null : xmlStringValue.getString();
192 }
193
194 /**
195 * @see DataValueDescriptor#getLength
196 */
197 public int getLength() throws StandardException
198 {
199 return ((xmlStringValue == null) ? 0 : xmlStringValue.getLength());
200 }
201
202 /**
203 * @see DataValueDescriptor#estimateMemoryUsage
204 */
205 public int estimateMemoryUsage()
206 {
207 int sz = BASE_MEMORY_USAGE;
208 if (xmlStringValue != null)
209 sz += xmlStringValue.estimateMemoryUsage();
210 return sz;
211 }
212
213 /**
214 * @see DataValueDescriptor#readExternalFromArray
215 */
216 public void readExternalFromArray(ArrayInputStream in)
217 throws IOException
218 {
219 if (xmlStringValue == null)
220 xmlStringValue = new SQLChar();
221
222 // Read the XML implementation id. Right now there's
223 // only one implementation (UTF-8 based), so we don't
224 // use this value. But if better implementations come
225 // up in the future, we'll have to use this impl id to
226 // figure out how to read the data.
227 in.readShort();
228
229 // Now just read the XML data as UTF-8.
230 xmlStringValue.readExternalFromArray(in);
231 }
232
233 /**
234 * @see DataValueDescriptor#setFrom
235 */
236 protected void setFrom(DataValueDescriptor theValue)
237 throws StandardException
238 {
239 if (xmlStringValue == null)
240 xmlStringValue = new SQLChar();
241 xmlStringValue.setValue(theValue.getString());
242 }
243
244 /**
245 * @see DataValueDescriptor#setValueFromResultSet
246 */
247 public final void setValueFromResultSet(
248 ResultSet resultSet, int colNumber, boolean isNullable)
249 throws SQLException
250 {
251 if (xmlStringValue == null)
252 xmlStringValue = new SQLChar();
253 xmlStringValue.setValue(resultSet.getString(colNumber));
254 }
255
256 /**
257 * Compare two XML DataValueDescriptors. NOTE: This method
258 * should only be used by the database store for the purpose of
259 * index positioning--comparisons of XML type are not allowed
260 * from the language side of things. That said, all store
261 * wants to do is order the NULLs, so we don't actually
262 * have to do a full comparison. Just return an order
263 * value based on whether or not this XML value and the
264 * other XML value are null. As mentioned in the "compare"
265 * method of DataValueDescriptor, nulls are considered
266 * equal to other nulls and less than all other values.
267 *
268 * An example of when this method might be used is if the
269 * user executed a query like:
270 *
271 * select i from x_table where x_col is not null
272 *
273 * @see DataValueDescriptor#compare
274 */
275 public int compare(DataValueDescriptor other)
276 throws StandardException
277 {
278 if (SanityManager.DEBUG) {
279 SanityManager.ASSERT(other instanceof XMLDataValue,
280 "Store should NOT have tried to compare an XML value " +
281 "with a non-XML value.");
282 }
283
284 if (isNull()) {
285 if (other.isNull())
286 // both null, so call them 'equal'.
287 return 0;
288 // This XML is 'less than' the other.
289 return -1;
290 }
291
292 if (other.isNull())
293 // This XML is 'greater than' the other.
294 return 1;
295
296 // Two non-null values: we shouldn't ever get here,
297 // since that would necessitate a comparsion of XML
298 // values, which isn't allowed.
299 if (SanityManager.DEBUG) {
300 SanityManager.THROWASSERT(
301 "Store tried to compare two non-null XML values, " +
302 "which isn't allowed.");
303 }
304 return 0;
305 }
306
307 /* ****
308 * Storable interface, implies Externalizable, TypedFormat
309 */
310
311 /**
312 * @see TypedFormat#getTypeFormatId
313 *
314 * From the engine's perspective, all XML implementations share
315 * the same format id.
316 */
317 public int getTypeFormatId() {
318 return StoredFormatIds.XML_ID;
319 }
320
321 /**
322 * @see Storable#isNull
323 */
324 public boolean isNull()
325 {
326 return ((xmlStringValue == null) || xmlStringValue.isNull());
327 }
328
329 /**
330 * @see Storable#restoreToNull
331 */
332 public void restoreToNull()
333 {
334 if (xmlStringValue != null)
335 xmlStringValue.restoreToNull();
336 }
337
338 /**
339 * Read an XML value from an input stream.
340 * @param in The stream from which we're reading.
341 */
342 public void readExternal(ObjectInput in) throws IOException
343 {
344 if (xmlStringValue == null)
345 xmlStringValue = new SQLChar();
346
347 // Read the XML implementation id. Right now there's
348 // only one implementation (UTF-8 based), so we don't
349 // use this value. But if better implementations come
350 // up in the future, we'll have to use this impl id to
351 // figure out how to read the data.
352 in.readShort();
353
354 // Now just read the XML data as UTF-8.
355 xmlStringValue.readExternal(in);
356 }
357
358 /**
359 * Write an XML value.
360 * @param out The stream to which we're writing.
361 */
362 public void writeExternal(ObjectOutput out) throws IOException
363 {
364 // never called when value is null
365 if (SanityManager.DEBUG)
366 SanityManager.ASSERT(!isNull());
367
368 // Write out the XML store impl id.
369 out.writeShort(UTF8_IMPL_ID);
370
371 // Now write out the data.
372 xmlStringValue.writeExternal(out);
373 }
374
375 /* ****
376 * StreamStorable interface
377 * */
378
379 /**
380 * @see StreamStorable#returnStream
381 */
382 public InputStream returnStream()
383 {
384 return
385 (xmlStringValue == null) ? null : xmlStringValue.returnStream();
386 }
387
388 /**
389 * @see StreamStorable#setStream
390 */
391 public void setStream(InputStream newStream)
392 {
393 if (xmlStringValue == null)
394 xmlStringValue = new SQLChar();
395
396 // The stream that we receive is for an XML data value,
397 // which means it has an XML implementation id stored
398 // at the front (we put it there when we wrote it out).
399 // If we leave that there we'll get a failure when
400 // our underlying SQLChar tries to read from the
401 // stream, because the extra impl id will throw
402 // off the UTF format. So we need to read in (and
403 // ignore) the impl id before using the stream.
404 try {
405 // 2 bytes equal a short, which is what an impl id is.
406 newStream.read();
407 newStream.read();
408 } catch (Exception e) {
409 if (SanityManager.DEBUG)
410 SanityManager.THROWASSERT("Failed to read impl id" +
411 "bytes in setStream.");
412 }
413
414 // Now go ahead and use the stream.
415 xmlStringValue.setStream(newStream);
416 }
417
418 /**
419 * @see StreamStorable#loadStream
420 */
421 public void loadStream() throws StandardException
422 {
423 getString();
424 }
425
426 /* ****
427 * XMLDataValue interface.
428 * */
429
430 /**
431 * Method to parse an XML string and, if it's valid,
432 * store the _parsed_ version for subsequent use.
433 * @param text The string value to check.
434 * @param preserveWS Whether or not to preserve
435 * ignorable whitespace.
436 * @return If 'text' constitutes a valid XML document,
437 * it has been stored in this XML value and nothing
438 * is returned; otherwise, an exception is thrown.
439 * @exception StandardException Thrown on parse error.
440 */
441 public void parseAndLoadXML(String text, boolean preserveWS)
442 throws StandardException
443 {
444 try {
445
446 if (preserveWS) {
447 // We're just going to use the text exactly as it
448 // is, so we just need to see if it parses.
449 loadSAXReader();
450 saxReader.parse(
451 new InputSource(new StringReader(text)));
452 }
453 else {
454 // We don't support this yet, so we shouldn't
455 // get here.
456 if (SanityManager.DEBUG)
457 SanityManager.THROWASSERT("Tried to STRIP whitespace " +
458 "but we shouldn't have made it this far");
459 }
460
461 } catch (Exception xe) {
462 // The text isn't a valid XML document. Throw a StandardException
463 // with the parse exception nested in it.
464 throw StandardException.newException(
465 SQLState.LANG_NOT_AN_XML_DOCUMENT, xe);
466 }
467
468 // If we get here, the text is valid XML so go ahead
469 // and load/store it.
470 if (xmlStringValue == null)
471 xmlStringValue = new SQLChar();
472 xmlStringValue.setValue(text);
473 return;
474 }
475
476 /**
477 * The SQL/XML XMLSerialize operator.
478 * Converts this XML value into a string with a user-specified
479 * type, and returns that string via the received StringDataValue
480 * (if the received StringDataValue is non-null; else a new
481 * StringDataValue is returned).
482 * @param result The result of a previous call to this method,
483 * null if not called yet.
484 * @param targetType The string type to which we want to serialize.
485 * @param targetWidth The width of the target type.
486 * @return A serialized (to string) version of this XML object,
487 * in the form of a StringDataValue object.
488 * @exception StandardException Thrown on error
489 */
490 public StringDataValue XMLSerialize(StringDataValue result,
491 int targetType, int targetWidth) throws StandardException
492 {
493 if (result == null) {
494 switch (targetType)
495 {
496 case Types.CHAR: result = new SQLChar(); break;
497 case Types.VARCHAR: result = new SQLVarchar(); break;
498 case Types.LONGVARCHAR: result = new SQLLongvarchar(); break;
499 case Types.CLOB: result = new SQLClob(); break;
500 default:
501 // Shouldn't ever get here, as this check was performed
502 // at bind time.
503
504 if (SanityManager.DEBUG) {
505 SanityManager.THROWASSERT(
506 "Should NOT have made it to XMLSerialize " +
507 "with a non-string target type.");
508 }
509 return null;
510 }
511 }
512
513 // Else we're reusing a StringDataValue. We only reuse
514 // the result if we're executing the _same_ XMLSERIALIZE
515 // call on multiple rows. That means that all rows
516 // must have the same result type (targetType) and thus
517 // we know that the StringDataValue already has the
518 // correct type. So we're set.
519
520 if (this.isNull()) {
521 // Attempts to serialize a null XML value lead to a null
522 // result (SQL/XML[2003] section 10.13).
523 result.setToNull();
524 return result;
525 }
526
527 // Get the XML value as a string. For this UTF-8 impl,
528 // we already have it as a string, so just use that.
529 result.setValue(xmlStringValue.getString());
530
531 // Seems wrong to trunc an XML document, as it then becomes non-
532 // well-formed and thus useless. So we throw an error (that's
533 // what the "true" in the next line says).
534 result.setWidth(targetWidth, 0, true);
535 return result;
536 }
537
538 /**
539 * The SQL/XML XMLExists operator.
540 * Takes an XML query expression (as a string) and an XML
541 * value and checks if at least one node in the XML
542 * value matches the query expression. NOTE: For now,
543 * the query expression must be XPath only (XQuery not
544 * supported).
545 * @param xExpr The query expression, as a string.
546 * @param xml The XML value being queried.
547 * @return True if the received query expression matches at
548 * least one node in the received XML value; unknown if
549 * either the query expression or the xml value is null;
550 * false otherwise.
551 * @exception StandardException Thrown on error
552 */
553 public BooleanDataValue XMLExists(StringDataValue xExpr,
554 XMLDataValue xml) throws StandardException
555 {
556 if ((xExpr == null) || xExpr.isNull())
557 // If the query is null, we assume unknown.
558 return SQLBoolean.unknownTruthValue();
559
560 if ((xml == null) || xml.isNull())
561 // Then per SQL/XML spec 8.4, we return UNKNOWN.
562 return SQLBoolean.unknownTruthValue();
563
564 return new SQLBoolean(xml.exists(xExpr.getString()));
565 }
566
567 /**
568 * Helper method for XMLExists.
569 * See if the received XPath expression returns at least
570 * one node when evaluated against _this_ XML value.
571 * @param xExpr The XPath expression.
572 * @return True if at least one node in this XML value
573 * matches the received xExpr; false otherwise.
574 */
575 public boolean exists(String xExpr) throws StandardException
576 {
577 // NOTE: At some point we'll probably need to implement some
578 // some kind of query cache so that we don't have to recompile
579 // the same query over and over for every single XML row
580 // in a table. That's what we do right now...
581
582 try {
583
584 xExpr = replaceDoubleQuotes(xExpr);
585 loadXSLTObjects();
586
587 // Take our simple stylesheet and plug in the query.
588 int pos = xsltStylesheet.indexOf(XPATH_PLACEHOLDER);
589 StringBuffer stylesheet = new StringBuffer(xsltStylesheet);
590 stylesheet.replace(pos, pos + XPATH_PLACEHOLDER.length(), xExpr);
591
592 // Create a Templates ContentHandler to handle parsing of the
593 // stylesheet.
594 TemplatesHandler templatesHandler =
595 saxTFactory.newTemplatesHandler();
596 xsltReader.setContentHandler(templatesHandler);
597
598 // Now parse the generic stylesheet we created.
599 xsltReader.parse(
600 new InputSource(new StringReader(stylesheet.toString())));
601
602 // Get the Templates object (generated during the parsing of
603 // the stylesheet) from the TemplatesHandler.
604 Templates compiledQuery = templatesHandler.getTemplates();
605
606 // Create a Transformer ContentHandler to handle parsing of
607 // the XML Source.
608 TransformerHandler transformerHandler
609 = saxTFactory.newTransformerHandler(compiledQuery);
610
611 // Reset the XMLReader's ContentHandler to the TransformerHandler.
612 xsltReader.setContentHandler(transformerHandler);
613
614 // Create an ExistsHandler. When the XSLT transformation
615 // occurs, a period (".") will be thrown to this handler
616 // (via a SAX 'characters' event) for every matching
617 // node that XSLT finds. This is how we know if a
618 // match was found.
619 ExistsHandler eH = new ExistsHandler();
620 transformerHandler.setResult(new SAXResult(eH));
621
622 // This call to "parse" is what does the query, because we
623 // passed in an XSLT handler with the compiled query above.
624 try {
625 xsltReader.parse(
626 new InputSource(new StringReader(getString())));
627 } catch (Throwable th) {
628 if (th.getMessage().indexOf(
629 "SAXException: " + QUERY_MATCH_STRING) == -1)
630 { // then this isn't the exception that means we have
631 // a match; so re-throw it.
632 throw new Exception(th.getMessage());
633 }
634 }
635
636 // Did we have any matches?
637 return eH.exists();
638
639 } catch (Exception xe) {
640 // We don't expect to get here. Turn it into a
641 // StandardException, then throw it.
642 throw StandardException.newException(
643 SQLState.LANG_UNEXPECTED_XML_EXCEPTION, xe);
644 }
645 }
646
647 /* ****
648 * Helper classes and methods.
649 * */
650
651 /**
652 * Load an XMLReader for SAX events that can be used
653 * for parsing XML data.
654 *
655 * This method is currently only used for XMLPARSE, and
656 * the SQL/XML[2003] spec says that XMLPARSE should NOT
657 * perform validation -- Seciont 6.11:
658 *
659 * "Perform a non-validating parse of a character string to
660 * produce an XML value."
661 *
662 * Thus, we make sure to disable validation on the XMLReader
663 * loaded here. At some point in the future we will probably
664 * want to add support for the XMLVALIDATE function--but until
665 * then, user is unable to validate the XML values s/he inserts.
666 *
667 * Note that, even with validation turned off, XMLPARSE
668 * _will_ still check the well-formedness of the values,
669 * and it _will_ still process DTDs to get default values,
670 * etc--but that's it; no validation errors will be thrown.
671 *
672 * For future reference: the features needed to perform
673 * validation (with Xerces) are:
674 *
675 * http://apache.org/xml/features/validation/schema
676 * http://apache.org/xml/features/validation/dynamic
677 */
678 protected void loadSAXReader() throws Exception
679 {
680 if (saxReader != null)
681 // already loaded.
682 return;
683
684 // Get an instance of an XMLReader.
685 saxReader = XMLReaderFactory.createXMLReader(XML_PARSER_CLASS);
686
687 // Turn off validation, since it's not allowed by
688 // SQL/XML[2003] spec.
689 saxReader.setFeature(
690 "http://xml.org/sax/features/validation", false);
691
692 // Make the parser namespace aware.
693 saxReader.setFeature(
694 "http://xml.org/sax/features/namespaces", true);
695
696 // We have to set the error handler in order to properly
697 // receive the parse errors.
698 saxReader.setErrorHandler(new XMLErrorHandler());
699 }
700
701 /**
702 * Prepare for an XSLT query by loading the objects
703 * required for such a query. We should only have
704 * to do this once per XML object.
705 */
706 private void loadXSLTObjects() throws SAXException
707 {
708 if (xsltReader != null)
709 // we already loaded everything.
710 return;
711
712 // Instantiate a TransformerFactory.
713 TransformerFactory tFactory = TransformerFactory.newInstance();
714
715 // Cast the TransformerFactory to SAXTransformerFactory.
716 saxTFactory = (TransformerFactoryImpl)tFactory;
717
718 // Get an XML reader.
719 xsltReader = XMLReaderFactory.createXMLReader(XML_PARSER_CLASS);
720
721 // Make the parser namespace aware. Note that because we
722 // only support a small subset of SQL/XML, and because we
723 // only allow XPath (as opposed to XQuery) expressions,
724 // there is no way for a user to specify namespace
725 // bindings as part of the XMLEXISTS operator. This means
726 // that in order to query for a node name, the user must
727 // use the XPath functions "name()" and "local-name()"
728 // in conjunction with XPath 1.0 'namespace' axis. For
729 // example:
730 //
731 // To see if any elements exist that have a specific name
732 // with ANY namespace:
733 // //child::*[local-name()="someName"]
734 //
735 // To see if any elements exist that have a specific name
736 // with NO namespace:
737 // //child::*[name()="someName"]
738 //
739 // To see if any elements exist that have a specific name
740 // in a specific namespace:
741 // //child::*[local-name()=''someName'' and
742 // namespace::*[string()=''http://www.some.namespace'']]
743 //
744 xsltReader.setFeature(
745 "http://xml.org/sax/features/namespaces", true);
746
747 // Create a very simple XSLT stylesheet. This stylesheet
748 // will execute the XPath expression and, for every match,
749 // write a period (".") to the ExistsHandler (see the exists()
750 // method above). Then, in order to see if at least one
751 // node matches, we just check to see if the ExistsHandler
752 // caught at least one 'characters' event. If it did, then
753 // we know we had a match.
754 if (xsltStylesheet == null) {
755 StringBuffer sb = new StringBuffer();
756 sb.append("<xsl:stylesheet version=\"1.0\"\n");
757 sb.append("xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n");
758 sb.append(" <xsl:template match=\"/\">\n"); // Search whole doc...
759 sb.append(" <xsl:for-each select=\""); // For every match...
760 sb.append(XPATH_PLACEHOLDER); // using XPath expr...
761 sb.append("\">.</xsl:for-each>\n"); // Write a "."
762 sb.append(" </xsl:template>\n");
763 sb.append("</xsl:stylesheet>\n");
764 xsltStylesheet = sb.toString();
765 }
766 }
767
768 /**
769 * Takes a string (which is an XPath query specified by
770 * the user) and replaces any double quotes with single
771 * quotes. We have to do this because a double quote
772 * in the XSLT stylesheet (which is where the user's
773 * query ends up) will be parsed as a query terminator
774 * thus will cause XSLT execution errors.
775 * @param queryText Text in which we want to replace double
776 * quotes.
777 * @return queryText with all double quotes replaced by
778 * single quotes.
779 */
780 private String replaceDoubleQuotes(String queryText)
781 {
782 int pos = queryText.indexOf("\"");
783 if (pos == -1)
784 // nothing to do.
785 return queryText;
786
787 StringBuffer sBuf = new StringBuffer(queryText);
788 while (pos >= 0) {
789 sBuf.replace(pos, pos+1, "'");
790 pos = queryText.indexOf("\"", pos+1);
791 }
792 return sBuf.toString();
793 }
794
795 /*
796 ** The XMLErrorHandler class is just a generic implementation
797 ** of the ErrorHandler interface. It allows us to catch
798 ** and process XML parsing errors in a graceful manner.
799 */
800 private class XMLErrorHandler implements ErrorHandler
801 {
802 public void error (SAXParseException exception)
803 throws SAXException
804 {
805 throw new SAXException (exception);
806 }
807
808 public void fatalError (SAXParseException exception)
809 throws SAXException
810 {
811 throw new SAXException (exception);
812 }
813
814 public void warning (SAXParseException exception)
815 throws SAXException
816 {
817 throw new SAXException (exception);
818 }
819 }
820
821 /*
822 ** The ExistsHandler is what we pass to the XSLT processor
823 ** when we query. The generic xsltStylesheet that we defined
824 ** above will throw a 'characters' event for every matching
825 ** node that is found by the XSLT transformation. This
826 ** handler is the one that catches the event, and thus
827 ** it tells us whether or not we had a match.
828 */
829 private class ExistsHandler extends DefaultHandler
830 {
831 // Did we catch at least one 'characters' event?
832 private boolean atLeastOneMatch;
833
834 public ExistsHandler() {
835 atLeastOneMatch = false;
836 }
837
838 /*
839 * Catch a SAX 'characters' event, which tells us that
840 * we had at least one matching node.
841 */
842 public void characters(char[] ch, int start, int length)
843 throws SAXException
844 {
845 // If we get here, we had at least one matching node.
846 // Since that's all we need to know, we don't have
847 // to continue querying--we can stop the XSLT
848 // transformation now by throwing a SAX exception.
849 atLeastOneMatch = true;
850 throw new SAXException(QUERY_MATCH_STRING);
851 }
852
853 /*
854 * Tell whether or not this handler caught a match.
855 */
856 public boolean exists()
857 {
858 return atLeastOneMatch;
859 }
860 }
861 }