001/*
002// $Id: //open/util/resgen/src/org/eigenbase/xom/DOMElementParser.java#6 $
003// Package org.eigenbase.xom is an XML Object Mapper.
004// Copyright (C) 2005-2005 The Eigenbase Project
005// Copyright (C) 2005-2005 Disruptive Tech
006// Copyright (C) 2005-2005 LucidEra, Inc.
007// Portions Copyright (C) 2000-2005 Kana Software, Inc. and others.
008//
009// This library is free software; you can redistribute it and/or modify it
010// under the terms of the GNU Lesser General Public License as published by the
011// Free Software Foundation; either version 2 of the License, or (at your
012// option) any later version approved by The Eigenbase Project.
013//
014// This library 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.  See the
017// GNU Lesser General Public License for more details.
018//
019// You should have received a copy of the GNU Lesser General Public License
020// along with this library; if not, write to the Free Software
021// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
022//
023// dsommerfield, 6 November, 2000
024*/
025
026package org.eigenbase.xom;
027import java.lang.reflect.Constructor;
028import java.lang.reflect.InvocationTargetException;
029import java.lang.reflect.Method;
030import java.lang.reflect.Modifier;
031import java.util.Vector;
032
033/**
034 * DOMElementParser is a utility wrapper around DOMWrapper.
035 * Implements a parseable stream of child DOMWrappers and also provides
036 * validation on an XML document beyond the DTD.
037 */
038public class DOMElementParser {
039
040    private DOMWrapper wrapper;
041    private DOMWrapper[] children;
042    private int currentIndex;
043    private DOMWrapper currentChild;
044
045    private int optionIndex;
046    private String prefix;
047    private Class enclosure;
048
049    /**
050     * Constructs a new ElementParser based on an Element of the XML parse
051     * tree wrapped in a DOMWrapper, and a prefix (to be applied to all element
052     * tags except the root), and the name of the enclosing class.
053     * @param wrapper a DOMWrapper representing the section of the XML parse tree
054     * to traverse.
055     */
056    public DOMElementParser(DOMWrapper wrapper, String prefix, Class enclosure)
057        throws XOMException
058    {
059        this.wrapper = wrapper;
060        children = wrapper.getElementChildren();
061        currentIndex = 0;
062        currentChild = null;
063        getNextElement();
064
065        this.prefix = prefix;
066        if (prefix == null) {
067            this.prefix = "";
068        }
069        this.enclosure = enclosure;
070    }
071
072    /**
073     * Private helper function to retrieve the next child element in sequence.
074     * @return the next element, or null if the enumerator has no more
075     * elements to return.
076     */
077    private void getNextElement()
078    {
079        if (currentIndex >= children.length) {
080            currentChild = null;
081        } else {
082            currentChild = children[currentIndex++];
083        }
084    }
085
086    /**
087     * Private helper function to verify that the next element matches a
088     * specific name.
089     * @param name name of the element to match.  Names are not case-sensitive.
090     * @throws XOMException if there is no current element or the names do
091     * not match.
092     */
093    private void requiredName(String name)
094        throws XOMException
095    {
096        String augName = prefix + name;
097        if (currentChild == null) {
098            throw new XOMException(
099                "Expected <" + augName + "> but found " + "nothing.");
100        } else if (!augName.equalsIgnoreCase(currentChild.getTagName())) {
101            throw new XOMException(
102                "Expected <" + augName + "> but found <"
103                    + currentChild.getTagName() + ">");
104        }
105    }
106
107    /**
108     * Private helper function to determine if the next element has the
109     * specified name.
110     * @return true if the next element's name matches <i>name</i>.  Matching
111     * is not case-sensitive.  Returns false if there is no next element or
112     * if the names don't match.
113     */
114    private boolean optionalName(String name)
115    {
116        String augName = prefix + name;
117        if (currentChild == null) {
118            return false;
119        } else if (augName.equalsIgnoreCase(currentChild.getTagName())) {
120            return true;
121        } else {
122            return false;
123        }
124    }
125
126    /**
127     * Returns the enclosure class associated with clazz, or falls back on
128     * the fixed enclosure if none can be found.
129     */
130    private Class getEnclosureClass(Class clazz)
131    {
132        // Instead of using a fixed enclosure, derive it from the given Class.
133        // If we can't figure it out, just use the given enclosure instead.
134        Class thisEnclosure = enclosure;
135        String className = clazz.getName();
136        int dollarPos = className.indexOf('$');
137        if (dollarPos >= 0) {
138            String encName = className.substring(0, dollarPos);
139            try {
140                thisEnclosure = Class.forName(encName);
141            } catch (ClassNotFoundException ex) {
142                throw new AssertFailure("Enclosure class " + encName
143                                 + " not found.");
144            }
145        }
146        return thisEnclosure;
147    }
148
149    /**
150     * Private helper function to determine if the next element's corresponding
151     * definition class is a subclass of the given class.  This may be used
152     * to detect if a name matches a class.
153     * @param clazz the class to match the next element against.
154     * @return true if the next element's name matches the given class, false
155     * otherwise.
156     * @throws XOMException if the next name is invalid (either doesn't
157     * start with DM or has no associated definition class).
158     */
159    private boolean nameMatchesClass(Class clazz)
160        throws XOMException
161    {
162        // Get the next name.  It must start with the set prefix, and it must
163        // match a definition in the enclosure class.
164        Class thisEnclosure = getEnclosureClass(clazz);
165        Class nextClass = ElementDef.getElementClass(currentChild,
166                                                     thisEnclosure,
167                                                     prefix);
168
169        // Determine if nextClass is a subclass of clazz.  Return true if so.
170        return nextClass != null &&
171                clazz.isAssignableFrom(nextClass);
172    }
173
174    /**
175     * This function retrieves a required String element from this parser,
176     * advancing the parser after the read.
177     * @param elementName the name of the element to retrieve.
178     * @return the String value stored inside the element to retrieve.
179     * @throws XOMException if there is no element with the given name.
180     */
181    public String requiredString(String elementName)
182        throws XOMException
183    {
184        requiredName(elementName);
185        String retval = currentChild.getText().trim();
186        getNextElement();
187        return retval;
188    }
189
190    /**
191     * This function retrieves an optional String element from this parser,
192     * advancing the parser if the element is found.
193     * If no element of the correct name is found, this function returns null.
194     * @param elementName the name of the element to retrieve.
195     * @return the String value stored inside the element to retrieve.
196     */
197    public String optionalString(String elementName)
198        throws XOMException
199    {
200        if (optionalName(elementName)) {
201            String retval = currentChild.getText().trim();
202            getNextElement();
203            return retval;
204        } else {
205            return null;
206        }
207    }
208
209    /**
210     * This function retrieves a required Element from this parser,
211     * advancing the parser after the read.
212     * @param elementName the name of the element to retrieve.
213     * @return the DOMWrapper to retrieve.
214     * @throws XOMException if there is no element with the given name.
215     */
216    public DOMWrapper requiredElement(String elementName)
217        throws XOMException
218    {
219        requiredName(elementName);
220        DOMWrapper prevWrapper = currentChild;
221        getNextElement();
222        return prevWrapper;
223    }
224
225    /**
226     * This function is used to return a CDATA section as text.  It does
227     * no parsing.
228     * @return the contents of the CDATA element as text.
229     */
230    public String getText()
231    {
232        return wrapper.getText().trim();
233    }
234
235    /**
236     * This function retrieves an optional Element from this parser,
237     * advancing the parser if the element is found.
238     * If no element of the correct name is found, this function returns null.
239     * @param elementName the name of the element to retrieve.
240     * @return the DOMWrapper to retreive, or null if none found.
241     */
242    public DOMWrapper optionalElement(String elementName)
243        throws XOMException
244    {
245        if (optionalName(elementName)) {
246            DOMWrapper prevChild = currentChild;
247            getNextElement();
248            return prevChild;
249        } else {
250            return null;
251        }
252    }
253
254    /**
255     * This private helper function formats a list of element names into
256     * a readable string for error messages.
257     */
258    private String formatOption(String[] elementNames)
259    {
260        StringBuffer sbuf = new StringBuffer();
261        for (int i = 0; i < elementNames.length; i++) {
262            sbuf.append("<DM" + prefix);
263            sbuf.append(elementNames[i]);
264            sbuf.append(">");
265            if (i < elementNames.length - 1) {
266                sbuf.append(" or ");
267            }
268        }
269        return sbuf.toString();
270    }
271
272    /**
273     * This function retrieves a required element which may have one of a
274     * number of names.  The parser is advanced after the read.
275     * @param elementNames an array of allowed names.  Names are compared in
276     * a case-insensitive fashion.
277     * @return the first element with one of the given names.
278     * @throws XOMException if there are no more elements to read or if
279     * the next element's name is not in the elementNames list.
280     */
281    public DOMWrapper requiredOption(String[] elementNames)
282        throws XOMException
283    {
284        if (currentChild == null) {
285            throw new XOMException("Expecting "
286                                      + formatOption(elementNames)
287                                      + " but found nothing.");
288        } else {
289            for (int i = 0; i < elementNames.length; i++) {
290                String augName = "DM" + elementNames[i];
291                if (augName.equalsIgnoreCase(
292                    currentChild.getTagName().toString())) {
293                    DOMWrapper prevWrapper = currentChild;
294                    getNextElement();
295                    optionIndex = i;
296                    return prevWrapper;
297                }
298            }
299
300            // If we got here, no names match.
301            throw new XOMException("Expecting "
302                                      + formatOption(elementNames)
303                                      + " but found <"
304                                      + currentChild.getTagName()
305                                      + ">.");
306        }
307    }
308
309    /**
310     * This function retrieves a required Element of a specific class
311     * from this parser, advancing the parser after the read.
312     * The class must be derived from ElementDef.
313     */
314    public NodeDef requiredClass(Class classTemplate)
315        throws XOMException
316    {
317        // The name must match the class.
318        if (!nameMatchesClass(classTemplate)) {
319            throw new XOMException("element <" + currentChild.getTagName()
320                                      + "> does not match expected class "
321                                      + classTemplate.getName());
322        }
323
324        // Get the class corresponding to the current tag
325        Class currentClass = ElementDef.getElementClass(currentChild,
326                                                        enclosure, prefix);
327
328        // Get the element
329        DOMWrapper prevWrapper = currentChild;
330        getNextElement();
331
332        // Construct an ElementDef of the correct class from the element
333        return ElementDef.constructElement(prevWrapper, currentClass);
334    }
335
336    /**
337     * Returns the option index of the element returned through the last
338     * requiredOption call.
339     */
340    public int lastOptionIndex()
341    {
342        return optionIndex;
343    }
344
345    /**
346     * This function retrieves a required Attribute by name from the
347     * current Element.
348     * @param attrName the name of the attribute.
349     * @return the String value of the attribute.
350     * @throws XOMException if no attribute of this name is set.
351     */
352    public String requiredAttribute(String attrName)
353        throws XOMException
354    {
355        Object attr = wrapper.getAttribute(attrName);
356        if (attr == null) {
357            throw new XOMException("Required attribute '"
358                                      + attrName + "' is not set.");
359        }
360        return attr.toString();
361    }
362
363    /**
364     * This static version of requiredAttribute uses any element definition
365     * as a basis for the attribute.  It is used by Plugin definitions to
366     * return attributes before the parser is created.
367     * @param wrapper the Element in which to find the attribute.
368     * @param attrName the name of the attribute to retrieve.
369     * @param defaultVal the default value of the attribute to retrieve.
370     * @throws XOMException if no attribute of this name is set.
371     */
372    public static String requiredDefAttribute(DOMWrapper wrapper,
373                                              String attrName,
374                                              String defaultVal)
375        throws XOMException
376    {
377        Object attr = wrapper.getAttribute(attrName);
378        if (attr == null) {
379            if (defaultVal == null) {
380                throw new XOMException("Required attribute "
381                                          + attrName + " is not set.");
382            } else {
383                return defaultVal;
384            }
385        }
386        return attr.toString();
387    }
388
389    /**
390     * This function retrieves an optional Attribute by name from the
391     * current Element.
392     * @param attrName the name of the attribute.
393     * @return the String value of the attribute, or null if the
394     * attribute is not set.
395     */
396    public String optionalAttribute(String attrName)
397        throws XOMException
398    {
399        Object attr = wrapper.getAttribute(attrName);
400        if (attr == null) {
401            return null;
402        }
403        return attr.toString();
404    }
405
406    /**
407     * This function retrieves an optional Attribute by name from the
408     * current Element, converting it to an Integer.
409     * @param attrName the name of the attribute.
410     * @return the Integer value of the attribute, or null if the
411     * attribute is not set.
412     * @throws XOMException if the value is set to an illegal
413     * integer value.
414     */
415    public Integer optionalIntegerAttribute(String attrName)
416        throws XOMException
417    {
418        Object attr = wrapper.getAttribute(attrName);
419        if (attr == null) {
420            return null;
421        }
422        try {
423            return new Integer(attr.toString());
424        } catch (NumberFormatException ex) {
425            throw new XOMException("Illegal integer value \""
426                                      + attr.toString() + "\" for attribute "
427                                      + attrName + ": " + ex.getMessage());
428        }
429    }
430
431   /**
432     * This function retrieves an optional Attribute by name from the
433     * current Element, converting it to a Double.
434     * @param attrName the name of the attribute.
435     * @return the Double value of the attribute, or null if the
436     * attribute is not set.
437     * @throws XOMException if the value is set to an illegal
438     * double value.
439     */
440    public Double optionalDoubleAttribute(String attrName)
441        throws XOMException
442    {
443        Object attr = wrapper.getAttribute(attrName);
444        if (attr == null) {
445            return null;
446        }
447        try {
448            return new Double(attr.toString());
449        } catch (NumberFormatException ex) {
450            throw new XOMException("Illegal double value \""
451                                      + attr.toString() + "\" for attribute "
452                                      + attrName + ": " + ex.getMessage());
453        }
454    }
455
456    /**
457     * This function retrieves an required Attribute by name from the
458     * current Element, converting it to an Integer.
459     * @param attrName the name of the attribute.
460     * @return the Integer value of the attribute.
461     * @throws XOMException if the value is not set, or is set to
462     * an illegal integer value.
463     */
464    public Integer requiredIntegerAttribute(String attrName)
465        throws XOMException
466    {
467        Object attr = wrapper.getAttribute(attrName);
468        if (attr == null) {
469            throw new XOMException("Required integer attribute "
470                                      + attrName + " is not set.");
471        }
472        try {
473            return new Integer(attr.toString());
474        } catch (NumberFormatException ex) {
475            throw new XOMException("Illegal integer value \""
476                                      + attr.toString() + "\" for attribute "
477                                      + attrName + ": " + ex.getMessage());
478        }
479    }
480
481    /**
482     * This function retrieves an optional Attribute by name from the
483     * current Element, converting it to an Boolean.  The string value
484     * "true" (in any case) is considered TRUE.  Any other value is
485     * considered false.
486     * @param attrName the name of the attribute.
487     * @return the Boolean value of the attribute, or null if the
488     * attribute is not set.
489     * @throws XOMException if the value is set to an illegal
490     * integer value.
491     */
492    public Boolean optionalBooleanAttribute(String attrName)
493        throws XOMException
494    {
495        Object attr = wrapper.getAttribute(attrName);
496        if (attr == null) {
497            return null;
498        }
499        return new Boolean(attr.toString());
500    }
501
502    /**
503     * This function retrieves an required Attribute by name from the
504     * current Element, converting it to a Boolean.  The string value
505     * "true" (in any case) is considered TRUE.  Any other value is
506     * considered false.
507     * @param attrName the name of the attribute.
508     * @return the Boolean value of the attribute.
509     */
510    public Boolean requiredBooleanAttribute(String attrName)
511        throws XOMException
512    {
513        Object attr = wrapper.getAttribute(attrName);
514        if (attr == null) {
515            throw new XOMException("Required boolean attribute "
516                                      + attrName + " is not set.");
517        }
518        return new Boolean(attr.toString());
519    }
520
521    /**
522     * This function retrieves a collection of elements with the given name,
523     * returning them as an array.
524     * @param elemName the element name.
525     * @param min the minimum number of elements required in the array.  Set
526     * this parameter to 0 to indicate no minimum.
527     * @param max the maximum number of elements allowed in the array.  Set
528     * this parameter to 0 to indicate no maximum.
529     * @return an Element array containing the discovered elements.
530     * @throws XOMException if there are fewer than min or more than max
531     * elements with the name <i>elemName</i>.
532     */
533    public DOMWrapper[] optionalArray(String elemName, int min, int max)
534        throws XOMException
535    {
536        // First, read the appropriate elements into a vector.
537        Vector vec = new Vector();
538        String augName = "DM" + elemName;
539        while (currentChild != null &&
540              augName.equalsIgnoreCase(currentChild.getTagName())) {
541            vec.addElement(currentChild);
542            getNextElement();
543        }
544
545        // Now, check for size violations
546        if (min > 0 && vec.size() < min) {
547            throw new XOMException("Expecting at least " + min + " <"
548                                      + elemName + "> but found " + vec.size());
549        }
550        if (max > 0 && vec.size() > max) {
551            throw new XOMException("Expecting at most " + max + " <"
552                                      + elemName + "> but found " +
553                                      vec.size());
554        }
555
556        // Finally, convert to an array and return.
557        DOMWrapper[] retval = new DOMWrapper[vec.size()];
558        for (int i = 0; i < retval.length; i++) {
559            retval[i] = (DOMWrapper)(vec.elementAt(i));
560        }
561        return retval;
562    }
563
564    /**
565     * This function retrieves a collection of elements which are subclasses of
566     * the given class, returning them as an array.  The array will contain
567     * ElementDef objects automatically constructed to be of the correct class.
568     * @param elemClass the element class.
569     * @param min the minimum number of elements required in the array.  Set
570     * this parameter to 0 to indicate no minimum.
571     * @param max the maximum number of elements allowed in the array.  Set
572     * this parameter to 0 to indicate no maximum.
573     * @return an ElementDef array containing the discovered elements.
574     * @throws XOMException if there are fewer than min or more than max
575     * elements with the name <i>elemName</i>.
576     */
577    public NodeDef[] classArray(Class elemClass, int min, int max)
578        throws XOMException
579    {
580        // Instead of using a fixed enclosure, derive it from the given Class.
581        // If we can't figure it out, just use the given enclosure instead.
582        Class thisEnclosure = getEnclosureClass(elemClass);
583
584        // First, read the appropriate elements into a vector.
585        Vector vec = new Vector();
586        while (currentChild != null &&
587              nameMatchesClass(elemClass)) {
588            vec.addElement(currentChild);
589            getNextElement();
590        }
591
592        // Now, check for size violations
593        if (min > 0 && vec.size() < min) {
594            throw new XOMException("Expecting at least " + min + " <"
595                                      + elemClass.getName()
596                                      + "> but found " + vec.size());
597        }
598        if (max > 0 && vec.size() > max) {
599            throw new XOMException("Expecting at most " + max + " <"
600                                      + elemClass.getName()
601                                      + "> but found " +
602                                      vec.size());
603        }
604
605        // Finally, convert to an array and return.
606        NodeDef[] retval = new NodeDef[vec.size()];
607        for (int i = 0; i < retval.length; i++) {
608            retval[i] =
609                ElementDef.constructElement((DOMWrapper)(vec.elementAt(i)),
610                                            thisEnclosure, prefix);
611        }
612        return retval;
613    }
614
615    /**
616     * This function retrieves an Element from this parser, advancing the
617     * parser if the element is found.  The Element's corresponding
618     * ElementDef class is looked up and its constructor is called
619     * automatically.  If the requested Element is not found the function
620     * returns null <i>unless</i> required is set to true.  In this case,
621     * a XOMException is thrown.
622     * @param elementClass the Class of the element to retrieve.
623     * @param required true to throw an exception if the element is not
624     * found, false to simply return null.
625     * @return the element, as an ElementDef, or null if it is not found
626     * and required is false.
627     * @throws XOMException if required is true and the element could not
628     * be found.
629     */
630    public NodeDef getElement(Class elementClass,
631                              boolean required)
632        throws XOMException
633    {
634        // If current element is null, return null immediately
635        if (currentChild == null) {
636            return null;
637        }
638
639        // Check if the name matches the class
640        if (!nameMatchesClass(elementClass)) {
641            if (required) {
642                throw new XOMException("element <" + currentChild.getTagName()
643                                          + "> is not of expected type "
644                                          + elementClass.getName());
645            } else {
646                return null;
647            }
648        }
649
650
651
652        // Get the class corresponding to the current tag.  This will be
653        // equal to elementClass if the current content was declared using
654        // an Element, but not if the current content was declared using
655        // a Class.
656        Class thisEnclosure = getEnclosureClass(elementClass);
657        Class currentClass = ElementDef.getElementClass(currentChild,
658                                                        thisEnclosure, prefix);
659
660        // Get the element
661        DOMWrapper prevChild = currentChild;
662        getNextElement();
663
664        // Construct an ElementDef of the correct class from the element
665        return ElementDef.constructElement(prevChild, currentClass);
666    }
667
668    /**
669     * This function retrieves a collection of elements which are subclasses of
670     * the given class, returning them as an array.  The array will contain
671     * ElementDef objects automatically constructed to be of the correct class.
672     * @param elemClass the element class.
673     * @param min the minimum number of elements required in the array.  Set
674     * this parameter to 0 to indicate no minimum.
675     * @param max the maximum number of elements allowed in the array.  Set
676     * this parameter to 0 to indicate no maximum.
677     * @return an ElementDef array containing the discovered elements.
678     * @throws XOMException if there are fewer than min or more than max
679     * elements with the name <i>elemName</i>.
680     */
681    public NodeDef[] getArray(Class elemClass, int min, int max)
682        throws XOMException
683    {
684        return classArray(elemClass, min, max);
685    }
686
687    /**
688     * This function retrieves a String element from this parser,
689     * advancing the parser if the element is found.
690     * If no element of the correct name is found, this function returns null,
691     * unless required is true, in which case a XOMException is thrown.
692     * @param elementName the name of the element to retrieve.
693     * @param required true to throw an exception if the element is not
694     * found, false to simply return null.
695     * @return the String value stored inside the element to retrieve, or
696     * null if no element with the given elementName could be found.
697     */
698    public String getString(String elementName, boolean required)
699        throws XOMException
700    {
701        boolean found;
702        if (required) {
703            requiredName(elementName);
704            found = true;
705        } else {
706            found = optionalName(elementName);
707        }
708        if (found) {
709            String retval = currentChild.getText().trim();
710            getNextElement();
711            return retval;
712        } else {
713            return null;
714        }
715    }
716
717    /**
718     * This function returns a collection of String elements of the given
719     * name, returning them as an array.
720     * @param elemName the element name.
721     * @param min the minimum number of elements required in the array.  Set
722     * this parameter to 0 to indicate no minimum.
723     * @param max the maximum number of elements allowed in the array.  Set
724     * this parameter to 0 to indicate no maximum.
725     * @return a String array containing the discovered elements.
726     * @throws XOMException if there are fewer than min or more than max
727     * elements with the name <i>elemName</i>.
728     */
729    public String[] getStringArray(String elemName, int min, int max)
730        throws XOMException
731    {
732        // First, read the appropriate elements into a vector.
733        Vector vec = new Vector();
734        String augName = prefix + elemName;
735        while (currentChild != null &&
736              augName.equalsIgnoreCase(currentChild.getTagName().toString())) {
737            vec.addElement(currentChild);
738            getNextElement();
739        }
740
741        // Now, check for size violations
742        if (min > 0 && vec.size() < min) {
743            throw new XOMException("Expecting at least " + min + " <"
744                                      + elemName + "> but found " + vec.size());
745        }
746        if (max > 0 && vec.size() > max) {
747            throw new XOMException("Expecting at most " + max + " <"
748                                      + elemName + "> but found " +
749                                      vec.size());
750        }
751
752        // Finally, convert to an array, retrieve the text from each
753        // element, and return.
754        String[] retval = new String[vec.size()];
755        for (int i = 0; i < retval.length; i++) {
756            retval[i] = ((DOMWrapper)(vec.elementAt(i))).getText().trim();
757        }
758        return retval;
759    }
760
761    // Determine if a String is present anywhere in a given array.
762    private boolean stringInArray(String str, String[] array)
763    {
764        for (int i = 0; i < array.length; i++) {
765            if (str.equals(array[i])) {
766                return true;
767            }
768        }
769        return false;
770    }
771
772    // Convert an array of Strings into a single String for display.
773    private String arrayToString(String[] array)
774    {
775        StringBuffer sbuf = new StringBuffer();
776        sbuf.append("{");
777        for (int i = 0; i < array.length; i++) {
778            sbuf.append(array[i]);
779            if (i < array.length - 1) {
780                sbuf.append(", ");
781            }
782        }
783        sbuf.append("}");
784        return sbuf.toString();
785    }
786
787    /**
788     * Get a Class object representing a plugin class, identified either
789     * directly by a Java package and Java class name, or indirectly
790     * by a Java package and Java class which defines a method called
791     * getXMLDefClass() to return the appropriate class.
792     * @param packageName the name of the Java package containing the
793     * plugin class.
794     * @param className the name of the plugin definition class.
795     * @throws XOMException if the plugin class cannot be located
796     * or if the designated class is not suitable as a plugin class.
797     */
798    public static Class getPluginClass(String packageName,
799                                       String className)
800        throws XOMException
801    {
802        Class managerClass = null;
803        try {
804            managerClass = Class.forName(packageName + "." + className);
805        } catch (ClassNotFoundException ex) {
806            throw new XOMException("Unable to locate plugin class "
807                                      + packageName + "."
808                                      + className + ": "
809                                      + ex.getMessage());
810        }
811
812        return getPluginClass(managerClass);
813    }
814
815    /**
816     * Get a Class object representing a plugin class, given a manager
817     * class that implements the static method getXMLDefClass().
818     * @param managerClass any Class that implements getXMLDefClass.
819     * @return the plugin Class.
820     */
821    public static Class getPluginClass(Class managerClass)
822        throws XOMException
823    {
824        // Look for a static method called getXMLDefClass which returns
825        // type Class.  If we find this method, call it to produce the
826        // actual plugin class.  Otherwise, throw an exception; the
827        // class we selected is inappropriate.
828        Method[] methods = managerClass.getMethods();
829        for (int i = 0; i < methods.length; i++) {
830            // Must be static, take no args, and return Class.
831            if (methods[i].getParameterTypes().length != 0) {
832                continue;
833            }
834            if (!(methods[i].getReturnType() == Class.class)) {
835                continue;
836            }
837            if (!(Modifier.isStatic(methods[i].getModifiers()))) {
838                continue;
839            }
840
841            // Invoke the method here.
842            try {
843                Object[] args = new Object[0];
844                return (Class)(methods[i].invoke(null, args));
845            } catch (InvocationTargetException ex) {
846                throw new XOMException("Exception while retrieving "
847                                          + "plugin class: " +
848                                          ex.getTargetException().toString());
849            } catch (IllegalAccessException ex) {
850                throw new XOMException("Illegal access while retrieving "
851                                          + "plugin class: " +
852                                          ex.getMessage());
853            }
854        }
855
856        // Class is inappropriate.
857        throw new XOMException("Plugin class " + managerClass.getName()
858                                  + " is not an appropriate plugin class; "
859                                  + "getXMLDefClass() is not defined.");
860    }
861
862    /**
863     * Retrieve an Attribute from the parser.  The Attribute may be of any
864     * Java class, provided that the class supports a constructor from the
865     * String class.  The Attribute's value will be returned as an Object,
866     * which must then be cast to the appropraite type.  If the attribute
867     * is not defined and has no default, either null is returned (if
868     * required is false), or a XOMException is thrown (if required is
869     * true).
870     * @param attrName the name of the attribute to retreive.
871     * @param attrType a String naming a Java Class to serve as the type.
872     * If attrType contains a "." character, the class is looked up directly
873     * from the type name.  Otherwise, the class is looked up in the
874     * java.lang package.  Finally, the class must have a constructor which
875     * takes a String as an argument.
876     * @param defaultValue the default value for this attribute.  If values
877     * is set, the defaultValue must also be one of the set of values.
878     * defaultValue may be null.
879     * @param values an array of possible values for the attribute.  If
880     * this parameter is not null, then the attribute's value must be one
881     * of the listed set of values or an exception will be thrown.
882     * @param required if set, then this function will throw an exception
883     * if the attribute has no value and defaultValue is null.
884     * @return the Attribute's value as an Object.  The actual class of
885     * this object is determined by attrType.
886     */
887    public Object getAttribute(String attrName, String attrType,
888                               String defaultValue, String[] values,
889                               boolean required)
890        throws XOMException
891    {
892        // Retrieve the attribute type class
893        if (attrType.indexOf('.') == -1) {
894            attrType = "java.lang." + attrType;
895        }
896        Class typeClass = null;
897        try {
898            typeClass = Class.forName(attrType);
899        } catch (ClassNotFoundException ex) {
900            throw new XOMException("Class could not be found for attribute "
901                                      + "type: " + attrType + ": "
902                                      + ex.getMessage());
903        }
904
905        // Get a constructor from the type class which takes a String as
906        // input.  If one does not exist, throw an exception.
907        Class[] classArray = new Class[1];
908        classArray[0] = java.lang.String.class;
909        Constructor stringConstructor = null;
910        try {
911            stringConstructor = typeClass.getConstructor(classArray);
912        } catch (NoSuchMethodException ex) {
913            throw new XOMException("Attribute type class " +
914                                      attrType + " does not have a "
915                                      + "constructor which takes a String: "
916                                      + ex.getMessage());
917        }
918
919        // Get the Attribute of the given name
920        Object attrVal = wrapper.getAttribute(attrName);
921        if (attrVal == null) {
922            attrVal = defaultValue;
923        }
924        // Check for null
925        if (attrVal == null) {
926            if (required) {
927                throw new XOMException(
928                    "Attribute '" + attrName +
929                    "' is unset and has no default value.");
930            } else {
931                return null;
932            }
933        }
934
935        // Make sure it is on the list of acceptable values
936        if (values != null) {
937            if (!stringInArray(attrVal.toString(), values)) {
938                throw new XOMException(
939                    "Value '" + attrVal.toString()
940                        + "' of attribute '"
941                        + attrName + "' has illegal value '"
942                        + attrVal + "'.  Legal values: "
943                        + arrayToString(values));
944            }
945        }
946
947        // Invoke the constructor to get the final object
948        Object[] args = new Object[1];
949        args[0] = attrVal.toString();
950        try {
951            return stringConstructor.newInstance(args);
952        } catch (InstantiationException ex) {
953            throw new XOMException(
954                "Unable to construct a " + attrType
955                    + " from value \"" + attrVal + "\": "
956                    + ex.getMessage());
957        } catch (InvocationTargetException ex) {
958            throw new XOMException(
959                "Unable to construct a " + attrType
960                    + " from value \"" + attrVal + "\": "
961                    + ex.getMessage());
962        } catch (IllegalAccessException ex) {
963            throw new XOMException(
964                "Unable to construct a " + attrType
965                    + " from value \"" + attrVal + "\": "
966                    + ex.getMessage());
967        }
968    }
969}
970
971// End DOMElementParser.java