001/*
002 * SVG Salamander
003 * Copyright (c) 2004, Mark McKay
004 * All rights reserved.
005 *
006 * Redistribution and use in source and binary forms, with or 
007 * without modification, are permitted provided that the following
008 * conditions are met:
009 *
010 *   - Redistributions of source code must retain the above 
011 *     copyright notice, this list of conditions and the following
012 *     disclaimer.
013 *   - Redistributions in binary form must reproduce the above
014 *     copyright notice, this list of conditions and the following
015 *     disclaimer in the documentation and/or other materials 
016 *     provided with the distribution.
017 *
018 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
019 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
020 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
021 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
022 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
023 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
025 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
026 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
027 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
028 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
029 * OF THE POSSIBILITY OF SUCH DAMAGE. 
030 * 
031 * Mark McKay can be contacted at mark@kitfox.com.  Salamander and other
032 * projects can be found at http://www.kitfox.com
033 *
034 * Created on February 18, 2004, 1:49 PM
035 */
036
037package com.kitfox.svg.xml;
038
039import com.kitfox.svg.SVGConst;
040import org.w3c.dom.*;
041import java.awt.*;
042import java.net.*;
043import java.util.*;
044import java.util.regex.*;
045import java.lang.reflect.*;
046import java.util.logging.Level;
047import java.util.logging.Logger;
048
049/**
050 * @author Mark McKay
051 * @author <a href="mailto:mark@kitfox.com">Mark McKay</a>
052 */
053public class XMLParseUtil
054{
055    static final Matcher fpMatch = Pattern.compile("([-+]?((\\d*\\.\\d+)|(\\d+))([eE][+-]?\\d+)?)(\\%|in|cm|mm|pt|pc|px|em|ex)?").matcher("");
056    static final Matcher intMatch = Pattern.compile("[-+]?\\d+").matcher("");
057
058    /** Creates a new instance of XMLParseUtil */
059    private XMLParseUtil()
060    {
061    }
062
063    /**
064     * Scans the tag's children and returns the first text element found
065     */
066    public static String getTagText(Element ele)
067    {
068        NodeList nl = ele.getChildNodes();
069        int size = nl.getLength();
070
071        Node node = null;
072        int i = 0;
073        for (; i < size; i++)
074        {
075            node = nl.item(i);
076            if (node instanceof Text) break;
077        }
078        if (i == size || node == null) return null;
079
080        return ((Text)node).getData();
081    }
082
083    /**
084     * Returns the first node that is a direct child of root with the coresponding
085     * name.  Does not search children of children.
086     */
087    public static Element getFirstChild(Element root, String name)
088    {
089        NodeList nl = root.getChildNodes();
090        int size = nl.getLength();
091        for (int i = 0; i < size; i++)
092        {
093            Node node = nl.item(i);
094            if (!(node instanceof Element)) continue;
095            Element ele = (Element)node;
096            if (ele.getTagName().equals(name)) return ele;
097        }
098
099        return null;
100    }
101
102    public static String[] parseStringList(String list)
103    {
104//        final Pattern patWs = Pattern.compile("\\s+");
105        final Matcher matchWs = Pattern.compile("[^\\s]+").matcher("");
106        matchWs.reset(list);
107
108        LinkedList matchList = new LinkedList();
109        while (matchWs.find())
110        {
111            matchList.add(matchWs.group());
112        }
113
114        String[] retArr = new String[matchList.size()];
115        return (String[])matchList.toArray(retArr);
116    }
117
118    public static boolean isDouble(String val)
119    {
120        fpMatch.reset(val);
121        return fpMatch.matches();
122    }
123    
124    public static double parseDouble(String val)
125    {
126        /*
127        if (val == null) return 0.0;
128
129        double retVal = 0.0;
130        try
131        { retVal = Double.parseDouble(val); }
132        catch (Exception e)
133        {}
134        return retVal;
135         */
136        return findDouble(val);
137    }
138
139    /**
140     * Searches the given string for the first floating point number it contains,
141     * parses and returns it.
142     */
143    public synchronized static double findDouble(String val)
144    {
145        if (val == null) return 0;
146
147        fpMatch.reset(val);
148        try
149        {
150            if (!fpMatch.find()) return 0;
151        }
152        catch (StringIndexOutOfBoundsException e)
153        {
154            Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 
155                "XMLParseUtil: regex parse problem: '" + val + "'", e);
156        }
157
158        val = fpMatch.group(1);
159        //System.err.println("Parsing " + val);
160
161        double retVal = 0;
162        try
163        { 
164            retVal = Double.parseDouble(val); 
165            
166            float pixPerInch;
167            try {
168                pixPerInch = (float)Toolkit.getDefaultToolkit().getScreenResolution();
169            }
170            catch (NoClassDefFoundError err)
171            {
172                //Default value for headless X servers
173                pixPerInch = 72;
174            }
175            final float inchesPerCm = .3936f;
176            final String units = fpMatch.group(6);
177            
178            if ("%".equals(units)) retVal /= 100;
179            else if ("in".equals(units))
180            {
181                retVal *= pixPerInch;
182            }
183            else if ("cm".equals(units))
184            {
185                retVal *= inchesPerCm * pixPerInch;
186            }
187            else if ("mm".equals(units))
188            {
189                retVal *= inchesPerCm * pixPerInch * .1f;
190            }
191            else if ("pt".equals(units))
192            {
193                retVal *= (1f / 72f) * pixPerInch;
194            }
195            else if ("pc".equals(units))
196            {
197                retVal *= (1f / 6f) * pixPerInch;
198            }
199        }
200        catch (Exception e)
201        {}
202        return retVal;
203    }
204
205    /**
206     * Scans an input string for double values.  For each value found, places
207     * in a list.  This method regards any characters not part of a floating
208     * point value to be seperators.  Thus this will parse whitespace seperated,
209     * comma seperated, and many other separation schemes correctly.
210     */
211    public synchronized static double[] parseDoubleList(String list)
212    {
213        if (list == null) return null;
214
215        fpMatch.reset(list);
216
217        LinkedList doubList = new LinkedList();
218        while (fpMatch.find())
219        {
220            String val = fpMatch.group(1);
221            doubList.add(Double.valueOf(val));
222        }
223
224        double[] retArr = new double[doubList.size()];
225        Iterator it = doubList.iterator();
226        int idx = 0;
227        while (it.hasNext())
228        {
229            retArr[idx++] = ((Double)it.next()).doubleValue();
230        }
231
232        return retArr;
233    }
234
235    public static float parseFloat(String val)
236    {
237        /*
238        if (val == null) return 0f;
239
240        float retVal = 0f;
241        try
242        { retVal = Float.parseFloat(val); }
243        catch (Exception e)
244        {}
245        return retVal;
246         */
247        return findFloat(val);
248    }
249
250    /**
251     * Searches the given string for the first floating point number it contains,
252     * parses and returns it.
253     */
254    public synchronized static float findFloat(String val)
255    {
256        if (val == null) return 0f;
257
258        fpMatch.reset(val);
259        if (!fpMatch.find()) return 0f;
260
261        val = fpMatch.group(1);
262        //System.err.println("Parsing " + val);
263
264        float retVal = 0f;
265        try
266        {
267            retVal = Float.parseFloat(val);
268            String units = fpMatch.group(6);
269            if ("%".equals(units)) retVal /= 100;
270        }
271        catch (Exception e)
272        {}
273        return retVal;
274    }
275
276    public synchronized static float[] parseFloatList(String list)
277    {
278        if (list == null) return null;
279
280        fpMatch.reset(list);
281
282        LinkedList floatList = new LinkedList();
283        while (fpMatch.find())
284        {
285            String val = fpMatch.group(1);
286            floatList.add(Float.valueOf(val));
287        }
288
289        float[] retArr = new float[floatList.size()];
290        Iterator it = floatList.iterator();
291        int idx = 0;
292        while (it.hasNext())
293        {
294            retArr[idx++] = ((Float)it.next()).floatValue();
295        }
296
297        return retArr;
298    }
299
300    public static int parseInt(String val)
301    {
302        if (val == null) return 0;
303
304        int retVal = 0;
305        try
306        { retVal = Integer.parseInt(val); }
307        catch (Exception e)
308        {}
309        return retVal;
310    }
311
312    /**
313     * Searches the given string for the first integer point number it contains,
314     * parses and returns it.
315     */
316    public static int findInt(String val)
317    {
318        if (val == null) return 0;
319
320        intMatch.reset(val);
321        if (!intMatch.find()) return 0;
322
323        val = intMatch.group();
324        //System.err.println("Parsing " + val);
325
326        int retVal = 0;
327        try
328        { retVal = Integer.parseInt(val); }
329        catch (Exception e)
330        {}
331        return retVal;
332    }
333
334    public static int[] parseIntList(String list)
335    {
336        if (list == null) return null;
337
338        intMatch.reset(list);
339
340        LinkedList intList = new LinkedList();
341        while (intMatch.find())
342        {
343            String val = intMatch.group();
344            intList.add(Integer.valueOf(val));
345        }
346
347        int[] retArr = new int[intList.size()];
348        Iterator it = intList.iterator();
349        int idx = 0;
350        while (it.hasNext())
351        {
352            retArr[idx++] = ((Integer)it.next()).intValue();
353        }
354
355        return retArr;
356    }
357/*
358    public static int parseHex(String val)
359    {
360        int retVal = 0;
361        
362        for (int i = 0; i < val.length(); i++)
363        {
364            retVal <<= 4;
365            
366            char ch = val.charAt(i);
367            if (ch >= '0' && ch <= '9')
368            {
369                retVal |= ch - '0';
370            }
371            else if (ch >= 'a' && ch <= 'z')
372            {
373                retVal |= ch - 'a' + 10;
374            }
375            else if (ch >= 'A' && ch <= 'Z')
376            {
377                retVal |= ch - 'A' + 10;
378            }
379            else throw new RuntimeException();
380        }
381        
382        return retVal;
383    }
384*/
385    /**
386     * The input string represents a ratio.  Can either be specified as a
387     * double number on the range of [0.0 1.0] or as a percentage [0% 100%]
388     */
389    public static double parseRatio(String val)
390    {
391        if (val == null || val.equals("")) return 0.0;
392
393        if (val.charAt(val.length() - 1) == '%')
394        {
395            parseDouble(val.substring(0, val.length() - 1));
396        }
397        return parseDouble(val);
398    }
399
400    public static NumberWithUnits parseNumberWithUnits(String val)
401    {
402        if (val == null) return null;
403
404        return new NumberWithUnits(val);
405    }
406/*
407    public static Color parseColor(String val)
408    {
409        Color retVal = null;
410
411        if (val.charAt(0) == '#')
412        {
413            String hexStrn = val.substring(1);
414            
415            if (hexStrn.length() == 3)
416            {
417                hexStrn = "" + hexStrn.charAt(0) + hexStrn.charAt(0) + hexStrn.charAt(1) + hexStrn.charAt(1) + hexStrn.charAt(2) + hexStrn.charAt(2);
418            }
419            int hexVal = parseHex(hexStrn);
420
421            retVal = new Color(hexVal);
422        }
423        else
424        {
425            final Matcher rgbMatch = Pattern.compile("rgb\\((\\d+),(\\d+),(\\d+)\\)", Pattern.CASE_INSENSITIVE).matcher("");
426
427            rgbMatch.reset(val);
428            if (rgbMatch.matches())
429            {
430                int r = Integer.parseInt(rgbMatch.group(1));
431                int g = Integer.parseInt(rgbMatch.group(2));
432                int b = Integer.parseInt(rgbMatch.group(3));
433                retVal = new Color(r, g, b);
434            }
435            else
436            {
437                Color lookupCol = ColorTable.instance().lookupColor(val);
438                if (lookupCol != null) retVal = lookupCol;
439            }
440        }
441
442        return retVal;
443    }
444*/
445    /**
446     * Parses the given attribute of this tag and returns it as a String.
447     */
448    public static String getAttribString(Element ele, String name)
449    {
450        return ele.getAttribute(name);
451    }
452
453    /**
454     * Parses the given attribute of this tag and returns it as an int.
455     */
456    public static int getAttribInt(Element ele, String name)
457    {
458        String sval = ele.getAttribute(name);
459        int val = 0;
460        try { val = Integer.parseInt(sval); } catch (Exception e) {}
461
462        return val;
463    }
464
465    /**
466     * Parses the given attribute of this tag as a hexadecimal encoded string and
467     * returns it as an int
468     */
469    public static int getAttribIntHex(Element ele, String name)
470    {
471        String sval = ele.getAttribute(name);
472        int val = 0;
473        try { val = Integer.parseInt(sval, 16); } catch (Exception e) {}
474
475        return val;
476    }
477
478    /**
479     * Parses the given attribute of this tag and returns it as a float
480     */
481    public static float getAttribFloat(Element ele, String name)
482    {
483        String sval = ele.getAttribute(name);
484        float val = 0.0f;
485        try { val = Float.parseFloat(sval); } catch (Exception e) {}
486
487        return val;
488    }
489
490    /**
491     * Parses the given attribute of this tag and returns it as a double.
492     */
493    public static double getAttribDouble(Element ele, String name)
494    {
495        String sval = ele.getAttribute(name);
496        double val = 0.0;
497        try { val = Double.parseDouble(sval); } catch (Exception e) {}
498
499        return val;
500    }
501
502    /**
503     * Parses the given attribute of this tag and returns it as a boolean.
504     * Essentially compares the lower case textual value to the string "true"
505     */
506    public static boolean getAttribBoolean(Element ele, String name)
507    {
508        String sval = ele.getAttribute(name);
509
510        return sval.toLowerCase().equals("true");
511    }
512
513    public static URL getAttribURL(Element ele, String name, URL docRoot)
514    {
515        String sval = ele.getAttribute(name);
516
517        URL url;
518        try
519        {
520            return new URL(docRoot, sval);
521        }
522        catch (Exception e)
523        {
524            return null;
525        }
526    }
527
528    /**
529     * Returns the first ReadableXMLElement with the given name
530     */
531    public static ReadableXMLElement getElement(Class classType, Element root, String name, URL docRoot)
532    {
533        if (root == null) return null;
534
535        //Do not process if not a LoadableObject
536        if (!ReadableXMLElement.class.isAssignableFrom(classType))
537        {
538            return null;
539        }
540
541        NodeList nl = root.getChildNodes();
542        int size = nl.getLength();
543        for (int i = 0; i < size; i++)
544        {
545            Node node = nl.item(i);
546            if (!(node instanceof Element)) continue;
547            Element ele = (Element)node;
548            if (!ele.getTagName().equals(name)) continue;
549
550            ReadableXMLElement newObj = null;
551            try
552            {
553                newObj = (ReadableXMLElement)classType.newInstance();
554            }
555            catch (Exception e)
556            {
557                Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, null, e);
558                continue;
559            }
560            newObj.read(ele, docRoot);
561
562            if (newObj == null) continue;
563
564            return newObj;
565        }
566
567        return null;
568    }
569
570    /**
571     * Returns a HashMap of nodes that are children of root.  All nodes will
572     * be of class classType and have a tag name of 'name'.  'key' is
573     * an attribute of tag 'name' who's string value will be used as the key
574     * in the HashMap
575     */
576    public static HashMap getElementHashMap(Class classType, Element root, String name, String key, URL docRoot)
577    {
578        if (root == null) return null;
579
580        //Do not process if not a LoadableObject
581        if (!ReadableXMLElement.class.isAssignableFrom(classType))
582        {
583            return null;
584        }
585
586        HashMap retMap = new HashMap();
587
588        NodeList nl = root.getChildNodes();
589        int size = nl.getLength();
590        for (int i = 0; i < size; i++)
591        {
592            Node node = nl.item(i);
593            if (!(node instanceof Element)) continue;
594            Element ele = (Element)node;
595            if (!ele.getTagName().equals(name)) continue;
596
597            ReadableXMLElement newObj = null;
598            try 
599            {
600                newObj = (ReadableXMLElement)classType.newInstance();
601            }
602            catch (Exception e)
603            {
604                Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, null, e);
605                continue;
606            }
607            newObj.read(ele, docRoot);
608
609            if (newObj == null) continue;
610
611            String keyVal = getAttribString(ele, key);
612            retMap.put(keyVal, newObj);
613        }
614
615        return retMap;
616    }
617
618    public static HashSet getElementHashSet(Class classType, Element root, String name, URL docRoot)
619    {
620        if (root == null) return null;
621
622        //Do not process if not a LoadableObject
623        if (!ReadableXMLElement.class.isAssignableFrom(classType))
624        {
625            return null;
626        }
627
628        HashSet retSet = new HashSet();
629
630        NodeList nl = root.getChildNodes();
631        int size = nl.getLength();
632        for (int i = 0; i < size; i++)
633        {
634            Node node = nl.item(i);
635            if (!(node instanceof Element)) continue;
636            Element ele = (Element)node;
637            if (!ele.getTagName().equals(name)) continue;
638
639            ReadableXMLElement newObj = null;
640            try 
641            {
642                newObj = (ReadableXMLElement)classType.newInstance();
643            }
644            catch (Exception e)
645            {
646                Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, null, e);
647                continue;
648            }
649            newObj.read(ele, docRoot);
650
651            if (newObj == null)
652            {
653                continue;
654            }
655
656            retSet.add(newObj);
657        }
658
659        return retSet;
660    }
661
662
663    public static LinkedList getElementLinkedList(Class classType, Element root, String name, URL docRoot)
664    {
665        if (root == null) return null;
666
667        //Do not process if not a LoadableObject
668        if (!ReadableXMLElement.class.isAssignableFrom(classType))
669        {
670            return null;
671        }
672
673        NodeList nl = root.getChildNodes();
674        LinkedList elementCache = new LinkedList();
675        int size = nl.getLength();
676        for (int i = 0; i < size; i++)
677        {
678            Node node = nl.item(i);
679            if (!(node instanceof Element)) continue;
680            Element ele = (Element)node;
681            if (!ele.getTagName().equals(name)) continue;
682
683            ReadableXMLElement newObj = null;
684            try 
685            { 
686                newObj = (ReadableXMLElement)classType.newInstance();
687            }
688            catch (Exception e)
689            {
690                Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, null, e);
691                continue;
692            }
693            newObj.read(ele, docRoot);
694
695            elementCache.addLast(newObj);
696        }
697
698        return elementCache;
699    }
700
701    public static Object[] getElementArray(Class classType, Element root, String name, URL docRoot)
702    {
703        if (root == null) return null;
704
705        //Do not process if not a LoadableObject
706        if (!ReadableXMLElement.class.isAssignableFrom(classType))
707        {
708            return null;
709        }
710
711        LinkedList elementCache = getElementLinkedList(classType, root, name, docRoot);
712
713        Object[] retArr = (Object[])Array.newInstance(classType, elementCache.size());
714        return elementCache.toArray(retArr);
715    }
716
717    /**
718     * Takes a number of tags of name 'name' that are children of 'root', and
719     * looks for attributes of 'attrib' on them.  Converts attributes to an
720     * int and returns in an array.
721     */
722    public static int[] getElementArrayInt(Element root, String name, String attrib)
723    {
724        if (root == null) return null;
725
726        NodeList nl = root.getChildNodes();
727        LinkedList elementCache = new LinkedList();
728        int size = nl.getLength();
729
730        for (int i = 0; i < size; i++)
731        {
732            Node node = nl.item(i);
733            if (!(node instanceof Element)) continue;
734            Element ele = (Element)node;
735            if (!ele.getTagName().equals(name)) continue;
736
737            String valS = ele.getAttribute(attrib);
738            int eleVal = 0;
739            try { eleVal = Integer.parseInt(valS); }
740            catch (Exception e) {}
741
742            elementCache.addLast(new Integer(eleVal));
743        }
744
745        int[] retArr = new int[elementCache.size()];
746        Iterator it = elementCache.iterator();
747        int idx = 0;
748        while (it.hasNext())
749        {
750            retArr[idx++] = ((Integer)it.next()).intValue();
751        }
752
753        return retArr;
754    }
755
756    /**
757     * Takes a number of tags of name 'name' that are children of 'root', and
758     * looks for attributes of 'attrib' on them.  Converts attributes to an
759     * int and returns in an array.
760     */
761    public static String[] getElementArrayString(Element root, String name, String attrib)
762    {
763        if (root == null) return null;
764
765        NodeList nl = root.getChildNodes();
766        LinkedList elementCache = new LinkedList();
767        int size = nl.getLength();
768
769        for (int i = 0; i < size; i++)
770        {
771            Node node = nl.item(i);
772            if (!(node instanceof Element)) continue;
773            Element ele = (Element)node;
774            if (!ele.getTagName().equals(name)) continue;
775
776            String valS = ele.getAttribute(attrib);
777
778            elementCache.addLast(valS);
779        }
780
781        String[] retArr = new String[elementCache.size()];
782        Iterator it = elementCache.iterator();
783        int idx = 0;
784        while (it.hasNext())
785        {
786            retArr[idx++] = (String)it.next();
787        }
788
789        return retArr;
790    }
791
792    /**
793     * Takes a CSS style string and retursn a hash of them.
794     * @param styleString - A CSS formatted string of styles.  Eg,
795     *     "font-size:12;fill:#d32c27;fill-rule:evenodd;stroke-width:1pt;"
796     */
797    public static HashMap parseStyle(String styleString) {
798        return parseStyle(styleString, new HashMap());
799    }
800
801    /**
802     * Takes a CSS style string and returns a hash of them.
803     * @param styleString - A CSS formatted string of styles.  Eg,
804     *     "font-size:12;fill:#d32c27;fill-rule:evenodd;stroke-width:1pt;"
805     * @param map - A map to which these styles will be added
806     */
807    public static HashMap parseStyle(String styleString, HashMap map) {
808        final Pattern patSemi = Pattern.compile(";");
809
810        String[] styles = patSemi.split(styleString);
811
812        for (int i = 0; i < styles.length; i++)
813        {
814            if (styles[i].length() == 0)
815            {
816                continue;
817            }
818
819            int colon = styles[i].indexOf(':');
820            if (colon == -1)
821            {
822                continue;
823            }
824
825            String key = styles[i].substring(0, colon).trim();
826            String value = styles[i].substring(colon + 1).trim();
827
828            map.put(key, new StyleAttribute(key, value));
829        }
830
831        return map;
832    }
833}