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<String> matchList = new LinkedList<String>();
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<Double> doubList = new LinkedList<Double>();
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<Double> 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<Float> floatList = new LinkedList<Float>();
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<Float> 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<Integer> intList = new LinkedList<Integer>();
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<Integer> 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        try
518        {
519            return new URL(docRoot, sval);
520        }
521        catch (Exception e)
522        {
523            return null;
524        }
525    }
526
527    /**
528     * Returns the first ReadableXMLElement with the given name
529     */
530    public static ReadableXMLElement getElement(Class<?> classType, Element root, String name, URL docRoot)
531    {
532        if (root == null) return null;
533
534        //Do not process if not a LoadableObject
535        if (!ReadableXMLElement.class.isAssignableFrom(classType))
536        {
537            return null;
538        }
539
540        NodeList nl = root.getChildNodes();
541        int size = nl.getLength();
542        for (int i = 0; i < size; i++)
543        {
544            Node node = nl.item(i);
545            if (!(node instanceof Element)) continue;
546            Element ele = (Element)node;
547            if (!ele.getTagName().equals(name)) continue;
548
549            ReadableXMLElement newObj = null;
550            try
551            {
552                newObj = (ReadableXMLElement)classType.newInstance();
553            }
554            catch (Exception e)
555            {
556                Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, null, e);
557                continue;
558            }
559            newObj.read(ele, docRoot);
560
561            if (newObj == null) continue;
562
563            return newObj;
564        }
565
566        return null;
567    }
568
569    /**
570     * Returns a HashMap of nodes that are children of root.  All nodes will
571     * be of class classType and have a tag name of 'name'.  'key' is
572     * an attribute of tag 'name' who's string value will be used as the key
573     * in the HashMap
574     */
575    public static HashMap<String, ReadableXMLElement> getElementHashMap(Class<?> classType, Element root, String name, String key, URL docRoot)
576    {
577        if (root == null) return null;
578
579        //Do not process if not a LoadableObject
580        if (!ReadableXMLElement.class.isAssignableFrom(classType))
581        {
582            return null;
583        }
584
585        HashMap<String, ReadableXMLElement> retMap = new HashMap<String, ReadableXMLElement>();
586
587        NodeList nl = root.getChildNodes();
588        int size = nl.getLength();
589        for (int i = 0; i < size; i++)
590        {
591            Node node = nl.item(i);
592            if (!(node instanceof Element)) continue;
593            Element ele = (Element)node;
594            if (!ele.getTagName().equals(name)) continue;
595
596            ReadableXMLElement newObj = null;
597            try 
598            {
599                newObj = (ReadableXMLElement)classType.newInstance();
600            }
601            catch (Exception e)
602            {
603                Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, null, e);
604                continue;
605            }
606            newObj.read(ele, docRoot);
607
608            if (newObj == null) continue;
609
610            String keyVal = getAttribString(ele, key);
611            retMap.put(keyVal, newObj);
612        }
613
614        return retMap;
615    }
616
617    public static HashSet<ReadableXMLElement> getElementHashSet(Class<?> classType, Element root, String name, URL docRoot)
618    {
619        if (root == null) return null;
620
621        //Do not process if not a LoadableObject
622        if (!ReadableXMLElement.class.isAssignableFrom(classType))
623        {
624            return null;
625        }
626
627        HashSet<ReadableXMLElement> retSet = new HashSet<ReadableXMLElement>();
628
629        NodeList nl = root.getChildNodes();
630        int size = nl.getLength();
631        for (int i = 0; i < size; i++)
632        {
633            Node node = nl.item(i);
634            if (!(node instanceof Element)) continue;
635            Element ele = (Element)node;
636            if (!ele.getTagName().equals(name)) continue;
637
638            ReadableXMLElement newObj = null;
639            try 
640            {
641                newObj = (ReadableXMLElement)classType.newInstance();
642            }
643            catch (Exception e)
644            {
645                Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, null, e);
646                continue;
647            }
648            newObj.read(ele, docRoot);
649
650            if (newObj == null)
651            {
652                continue;
653            }
654
655            retSet.add(newObj);
656        }
657
658        return retSet;
659    }
660
661
662    public static LinkedList<ReadableXMLElement> getElementLinkedList(Class<?> classType, Element root, String name, URL docRoot)
663    {
664        if (root == null) return null;
665
666        //Do not process if not a LoadableObject
667        if (!ReadableXMLElement.class.isAssignableFrom(classType))
668        {
669            return null;
670        }
671
672        NodeList nl = root.getChildNodes();
673        LinkedList<ReadableXMLElement> elementCache = new LinkedList<ReadableXMLElement>();
674        int size = nl.getLength();
675        for (int i = 0; i < size; i++)
676        {
677            Node node = nl.item(i);
678            if (!(node instanceof Element)) continue;
679            Element ele = (Element)node;
680            if (!ele.getTagName().equals(name)) continue;
681
682            ReadableXMLElement newObj = null;
683            try 
684            { 
685                newObj = (ReadableXMLElement)classType.newInstance();
686            }
687            catch (Exception e)
688            {
689                Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, null, e);
690                continue;
691            }
692            newObj.read(ele, docRoot);
693
694            elementCache.addLast(newObj);
695        }
696
697        return elementCache;
698    }
699
700    public static Object[] getElementArray(Class<?> classType, Element root, String name, URL docRoot)
701    {
702        if (root == null) return null;
703
704        //Do not process if not a LoadableObject
705        if (!ReadableXMLElement.class.isAssignableFrom(classType))
706        {
707            return null;
708        }
709
710        LinkedList<ReadableXMLElement> elementCache = getElementLinkedList(classType, root, name, docRoot);
711
712        Object[] retArr = (Object[])Array.newInstance(classType, elementCache.size());
713        return elementCache.toArray(retArr);
714    }
715
716    /**
717     * Takes a number of tags of name 'name' that are children of 'root', and
718     * looks for attributes of 'attrib' on them.  Converts attributes to an
719     * int and returns in an array.
720     */
721    public static int[] getElementArrayInt(Element root, String name, String attrib)
722    {
723        if (root == null) return null;
724
725        NodeList nl = root.getChildNodes();
726        LinkedList<Integer> elementCache = new LinkedList<Integer>();
727        int size = nl.getLength();
728
729        for (int i = 0; i < size; i++)
730        {
731            Node node = nl.item(i);
732            if (!(node instanceof Element)) continue;
733            Element ele = (Element)node;
734            if (!ele.getTagName().equals(name)) continue;
735
736            String valS = ele.getAttribute(attrib);
737            int eleVal = 0;
738            try { eleVal = Integer.parseInt(valS); }
739            catch (Exception e) {}
740
741            elementCache.addLast(new Integer(eleVal));
742        }
743
744        int[] retArr = new int[elementCache.size()];
745        Iterator<Integer> it = elementCache.iterator();
746        int idx = 0;
747        while (it.hasNext())
748        {
749            retArr[idx++] = it.next().intValue();
750        }
751
752        return retArr;
753    }
754
755    /**
756     * Takes a number of tags of name 'name' that are children of 'root', and
757     * looks for attributes of 'attrib' on them.  Converts attributes to an
758     * int and returns in an array.
759     */
760    public static String[] getElementArrayString(Element root, String name, String attrib)
761    {
762        if (root == null) return null;
763
764        NodeList nl = root.getChildNodes();
765        LinkedList<String> elementCache = new LinkedList<String>();
766        int size = nl.getLength();
767
768        for (int i = 0; i < size; i++)
769        {
770            Node node = nl.item(i);
771            if (!(node instanceof Element)) continue;
772            Element ele = (Element)node;
773            if (!ele.getTagName().equals(name)) continue;
774
775            String valS = ele.getAttribute(attrib);
776
777            elementCache.addLast(valS);
778        }
779
780        String[] retArr = new String[elementCache.size()];
781        Iterator<String> it = elementCache.iterator();
782        int idx = 0;
783        while (it.hasNext())
784        {
785            retArr[idx++] = it.next();
786        }
787
788        return retArr;
789    }
790
791    /**
792     * Takes a CSS style string and retursn a hash of them.
793     * @param styleString - A CSS formatted string of styles.  Eg,
794     *     "font-size:12;fill:#d32c27;fill-rule:evenodd;stroke-width:1pt;"
795     */
796    public static HashMap<String, StyleAttribute> parseStyle(String styleString) {
797        return parseStyle(styleString, new HashMap<String, StyleAttribute>());
798    }
799
800    /**
801     * Takes a CSS style string and returns a hash of them.
802     * @param styleString - A CSS formatted string of styles.  Eg,
803     *     "font-size:12;fill:#d32c27;fill-rule:evenodd;stroke-width:1pt;"
804     * @param map - A map to which these styles will be added
805     */
806    public static HashMap<String, StyleAttribute> parseStyle(String styleString, HashMap<String, StyleAttribute> map) {
807        final Pattern patSemi = Pattern.compile(";");
808
809        String[] styles = patSemi.split(styleString);
810
811        for (int i = 0; i < styles.length; i++)
812        {
813            if (styles[i].length() == 0)
814            {
815                continue;
816            }
817
818            int colon = styles[i].indexOf(':');
819            if (colon == -1)
820            {
821                continue;
822            }
823
824            String key = styles[i].substring(0, colon).trim();
825            String value = styles[i].substring(colon + 1).trim();
826
827            map.put(key, new StyleAttribute(key, value));
828        }
829
830        return map;
831    }
832}