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    
026    package org.eigenbase.xom;
027    import java.lang.reflect.Constructor;
028    import java.lang.reflect.InvocationTargetException;
029    import java.lang.reflect.Method;
030    import java.lang.reflect.Modifier;
031    import 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     */
038    public 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