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    static final Matcher quoteMatch = Pattern.compile("^'|'$").matcher("");
058
059    /** Creates a new instance of XMLParseUtil */
060    private XMLParseUtil()
061    {
062    }
063
064    /**
065     * Scans the tag's children and returns the first text element found
066     */
067    public static String getTagText(Element ele)
068    {
069        NodeList nl = ele.getChildNodes();
070        int size = nl.getLength();
071
072        Node node = null;
073        int i = 0;
074        for (; i < size; i++)
075        {
076            node = nl.item(i);
077            if (node instanceof Text) break;
078        }
079        if (i == size || node == null) return null;
080
081        return ((Text)node).getData();
082    }
083
084    /**
085     * Returns the first node that is a direct child of root with the coresponding
086     * name.  Does not search children of children.
087     */
088    public static Element getFirstChild(Element root, String name)
089    {
090        NodeList nl = root.getChildNodes();
091        int size = nl.getLength();
092        for (int i = 0; i < size; i++)
093        {
094            Node node = nl.item(i);
095            if (!(node instanceof Element)) continue;
096            Element ele = (Element)node;
097            if (ele.getTagName().equals(name)) return ele;
098        }
099
100        return null;
101    }
102
103    public static String[] parseStringList(String list)
104    {
105//        final Pattern patWs = Pattern.compile("\\s+");
106        final Matcher matchWs = Pattern.compile("[^\\s]+").matcher("");
107        matchWs.reset(list);
108
109        LinkedList<String> matchList = new LinkedList<String>();
110        while (matchWs.find())
111        {
112            matchList.add(matchWs.group());
113        }
114
115        String[] retArr = new String[matchList.size()];
116        return (String[])matchList.toArray(retArr);
117    }
118
119    public static boolean isDouble(String val)
120    {
121        fpMatch.reset(val);
122        return fpMatch.matches();
123    }
124    
125    public static double parseDouble(String val)
126    {
127        /*
128        if (val == null) return 0.0;
129
130        double retVal = 0.0;
131        try
132        { retVal = Double.parseDouble(val); }
133        catch (Exception e)
134        {}
135        return retVal;
136         */
137        return findDouble(val);
138    }
139
140    /**
141     * Searches the given string for the first floating point number it contains,
142     * parses and returns it.
143     */
144    public synchronized static double findDouble(String val)
145    {
146        if (val == null) return 0;
147
148        fpMatch.reset(val);
149        try
150        {
151            if (!fpMatch.find()) return 0;
152        }
153        catch (StringIndexOutOfBoundsException e)
154        {
155            Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 
156                "XMLParseUtil: regex parse problem: '" + val + "'", e);
157        }
158
159        val = fpMatch.group(1);
160        //System.err.println("Parsing " + val);
161
162        double retVal = 0;
163        try
164        { 
165            retVal = Double.parseDouble(val); 
166            
167            float pixPerInch;
168            try {
169                pixPerInch = (float)Toolkit.getDefaultToolkit().getScreenResolution();
170            }
171            catch (NoClassDefFoundError err)
172            {
173                //Default value for headless X servers
174                pixPerInch = 72;
175            }
176            final float inchesPerCm = .3936f;
177            final String units = fpMatch.group(6);
178            
179            if ("%".equals(units)) retVal /= 100;
180            else if ("in".equals(units))
181            {
182                retVal *= pixPerInch;
183            }
184            else if ("cm".equals(units))
185            {
186                retVal *= inchesPerCm * pixPerInch;
187            }
188            else if ("mm".equals(units))
189            {
190                retVal *= inchesPerCm * pixPerInch * .1f;
191            }
192            else if ("pt".equals(units))
193            {
194                retVal *= (1f / 72f) * pixPerInch;
195            }
196            else if ("pc".equals(units))
197            {
198                retVal *= (1f / 6f) * pixPerInch;
199            }
200        }
201        catch (Exception e)
202        {}
203        return retVal;
204    }
205
206    /**
207     * Scans an input string for double values.  For each value found, places
208     * in a list.  This method regards any characters not part of a floating
209     * point value to be seperators.  Thus this will parse whitespace seperated,
210     * comma seperated, and many other separation schemes correctly.
211     */
212    public synchronized static double[] parseDoubleList(String list)
213    {
214        if (list == null) return null;
215
216        fpMatch.reset(list);
217
218        LinkedList<Double> doubList = new LinkedList<Double>();
219        while (fpMatch.find())
220        {
221            String val = fpMatch.group(1);
222            doubList.add(Double.valueOf(val));
223        }
224
225        double[] retArr = new double[doubList.size()];
226        Iterator<Double> it = doubList.iterator();
227        int idx = 0;
228        while (it.hasNext())
229        {
230            retArr[idx++] = ((Double)it.next()).doubleValue();
231        }
232
233        return retArr;
234    }
235
236    public static float parseFloat(String val)
237    {
238        /*
239        if (val == null) return 0f;
240
241        float retVal = 0f;
242        try
243        { retVal = Float.parseFloat(val); }
244        catch (Exception e)
245        {}
246        return retVal;
247         */
248        return findFloat(val);
249    }
250
251    /**
252     * Searches the given string for the first floating point number it contains,
253     * parses and returns it.
254     */
255    public synchronized static float findFloat(String val)
256    {
257        if (val == null) return 0f;
258
259        fpMatch.reset(val);
260        if (!fpMatch.find()) return 0f;
261
262        val = fpMatch.group(1);
263        //System.err.println("Parsing " + val);
264
265        float retVal = 0f;
266        try
267        {
268            retVal = Float.parseFloat(val);
269            String units = fpMatch.group(6);
270            if ("%".equals(units)) retVal /= 100;
271        }
272        catch (Exception e)
273        {}
274        return retVal;
275    }
276
277    public synchronized static float[] parseFloatList(String list)
278    {
279        if (list == null) return null;
280
281        fpMatch.reset(list);
282
283        LinkedList<Float> floatList = new LinkedList<Float>();
284        while (fpMatch.find())
285        {
286            String val = fpMatch.group(1);
287            floatList.add(Float.valueOf(val));
288        }
289
290        float[] retArr = new float[floatList.size()];
291        Iterator<Float> it = floatList.iterator();
292        int idx = 0;
293        while (it.hasNext())
294        {
295            retArr[idx++] = ((Float)it.next()).floatValue();
296        }
297
298        return retArr;
299    }
300
301    public static int parseInt(String val)
302    {
303        if (val == null) return 0;
304
305        int retVal = 0;
306        try
307        { retVal = Integer.parseInt(val); }
308        catch (Exception e)
309        {}
310        return retVal;
311    }
312
313    /**
314     * Searches the given string for the first integer point number it contains,
315     * parses and returns it.
316     */
317    public static int findInt(String val)
318    {
319        if (val == null) return 0;
320
321        intMatch.reset(val);
322        if (!intMatch.find()) return 0;
323
324        val = intMatch.group();
325        //System.err.println("Parsing " + val);
326
327        int retVal = 0;
328        try
329        { retVal = Integer.parseInt(val); }
330        catch (Exception e)
331        {}
332        return retVal;
333    }
334
335    public static int[] parseIntList(String list)
336    {
337        if (list == null) return null;
338
339        intMatch.reset(list);
340
341        LinkedList<Integer> intList = new LinkedList<Integer>();
342        while (intMatch.find())
343        {
344            String val = intMatch.group();
345            intList.add(Integer.valueOf(val));
346        }
347
348        int[] retArr = new int[intList.size()];
349        Iterator<Integer> it = intList.iterator();
350        int idx = 0;
351        while (it.hasNext())
352        {
353            retArr[idx++] = ((Integer)it.next()).intValue();
354        }
355
356        return retArr;
357    }
358/*
359    public static int parseHex(String val)
360    {
361        int retVal = 0;
362        
363        for (int i = 0; i < val.length(); i++)
364        {
365            retVal <<= 4;
366            
367            char ch = val.charAt(i);
368            if (ch >= '0' && ch <= '9')
369            {
370                retVal |= ch - '0';
371            }
372            else if (ch >= 'a' && ch <= 'z')
373            {
374                retVal |= ch - 'a' + 10;
375            }
376            else if (ch >= 'A' && ch <= 'Z')
377            {
378                retVal |= ch - 'A' + 10;
379            }
380            else throw new RuntimeException();
381        }
382        
383        return retVal;
384    }
385*/
386    /**
387     * The input string represents a ratio.  Can either be specified as a
388     * double number on the range of [0.0 1.0] or as a percentage [0% 100%]
389     */
390    public static double parseRatio(String val)
391    {
392        if (val == null || val.equals("")) return 0.0;
393
394        if (val.charAt(val.length() - 1) == '%')
395        {
396            parseDouble(val.substring(0, val.length() - 1));
397        }
398        return parseDouble(val);
399    }
400
401    public static NumberWithUnits parseNumberWithUnits(String val)
402    {
403        if (val == null) return null;
404
405        return new NumberWithUnits(val);
406    }
407/*
408    public static Color parseColor(String val)
409    {
410        Color retVal = null;
411
412        if (val.charAt(0) == '#')
413        {
414            String hexStrn = val.substring(1);
415            
416            if (hexStrn.length() == 3)
417            {
418                hexStrn = "" + hexStrn.charAt(0) + hexStrn.charAt(0) + hexStrn.charAt(1) + hexStrn.charAt(1) + hexStrn.charAt(2) + hexStrn.charAt(2);
419            }
420            int hexVal = parseHex(hexStrn);
421
422            retVal = new Color(hexVal);
423        }
424        else
425        {
426            final Matcher rgbMatch = Pattern.compile("rgb\\((\\d+),(\\d+),(\\d+)\\)", Pattern.CASE_INSENSITIVE).matcher("");
427
428            rgbMatch.reset(val);
429            if (rgbMatch.matches())
430            {
431                int r = Integer.parseInt(rgbMatch.group(1));
432                int g = Integer.parseInt(rgbMatch.group(2));
433                int b = Integer.parseInt(rgbMatch.group(3));
434                retVal = new Color(r, g, b);
435            }
436            else
437            {
438                Color lookupCol = ColorTable.instance().lookupColor(val);
439                if (lookupCol != null) retVal = lookupCol;
440            }
441        }
442
443        return retVal;
444    }
445*/
446    /**
447     * Parses the given attribute of this tag and returns it as a String.
448     */
449    public static String getAttribString(Element ele, String name)
450    {
451        return ele.getAttribute(name);
452    }
453
454    /**
455     * Parses the given attribute of this tag and returns it as an int.
456     */
457    public static int getAttribInt(Element ele, String name)
458    {
459        String sval = ele.getAttribute(name);
460        int val = 0;
461        try { val = Integer.parseInt(sval); } catch (Exception e) {}
462
463        return val;
464    }
465
466    /**
467     * Parses the given attribute of this tag as a hexadecimal encoded string and
468     * returns it as an int
469     */
470    public static int getAttribIntHex(Element ele, String name)
471    {
472        String sval = ele.getAttribute(name);
473        int val = 0;
474        try { val = Integer.parseInt(sval, 16); } catch (Exception e) {}
475
476        return val;
477    }
478
479    /**
480     * Parses the given attribute of this tag and returns it as a float
481     */
482    public static float getAttribFloat(Element ele, String name)
483    {
484        String sval = ele.getAttribute(name);
485        float val = 0.0f;
486        try { val = Float.parseFloat(sval); } catch (Exception e) {}
487
488        return val;
489    }
490
491    /**
492     * Parses the given attribute of this tag and returns it as a double.
493     */
494    public static double getAttribDouble(Element ele, String name)
495    {
496        String sval = ele.getAttribute(name);
497        double val = 0.0;
498        try { val = Double.parseDouble(sval); } catch (Exception e) {}
499
500        return val;
501    }
502
503    /**
504     * Parses the given attribute of this tag and returns it as a boolean.
505     * Essentially compares the lower case textual value to the string "true"
506     */
507    public static boolean getAttribBoolean(Element ele, String name)
508    {
509        String sval = ele.getAttribute(name);
510
511        return sval.toLowerCase().equals("true");
512    }
513
514    public static URL getAttribURL(Element ele, String name, URL docRoot)
515    {
516        String sval = ele.getAttribute(name);
517
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<String, ReadableXMLElement> 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<String, ReadableXMLElement> retMap = new HashMap<String, ReadableXMLElement>();
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<ReadableXMLElement> 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<ReadableXMLElement> retSet = new HashSet<ReadableXMLElement>();
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<ReadableXMLElement> 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<ReadableXMLElement> elementCache = new LinkedList<ReadableXMLElement>();
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<ReadableXMLElement> 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<Integer> elementCache = new LinkedList<Integer>();
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<Integer> it = elementCache.iterator();
747        int idx = 0;
748        while (it.hasNext())
749        {
750            retArr[idx++] = 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<String> elementCache = new LinkedList<String>();
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<String> it = elementCache.iterator();
783        int idx = 0;
784        while (it.hasNext())
785        {
786            retArr[idx++] = 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<String, StyleAttribute> parseStyle(String styleString) {
798        return parseStyle(styleString, new HashMap<String, StyleAttribute>());
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<String, StyleAttribute> parseStyle(String styleString, HashMap<String, StyleAttribute> 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 = quoteMatch.reset(styles[i].substring(colon + 1).trim()).replaceAll("");
827
828            map.put(key, new StyleAttribute(key, value));
829        }
830
831        return map;
832    }
833}