001    /*
002     * SVGUniverse.java
003     *
004     *
005     *  The Salamander Project - 2D and 3D graphics libraries in Java
006     *  Copyright (C) 2004 Mark McKay
007     *
008     *  This library is free software; you can redistribute it and/or
009     *  modify it under the terms of the GNU Lesser General Public
010     *  License as published by the Free Software Foundation; either
011     *  version 2.1 of the License, or (at your option) any later version.
012     *
013     *  This library is distributed in the hope that it will be useful,
014     *  but WITHOUT ANY WARRANTY; without even the implied warranty of
015     *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
016     *  Lesser General Public License for more details.
017     *
018     *  You should have received a copy of the GNU Lesser General Public
019     *  License along with this library; if not, write to the Free Software
020     *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
021     *
022     *  Mark McKay can be contacted at mark@kitfox.com.  Salamander and other
023     *  projects can be found at http://www.kitfox.com
024     *
025     * Created on February 18, 2004, 11:43 PM
026     */
027    
028    package com.kitfox.svg;
029    
030    import com.kitfox.svg.app.beans.SVGIcon;
031    import java.awt.Graphics2D;
032    import java.awt.image.BufferedImage;
033    import java.beans.PropertyChangeListener;
034    import java.beans.PropertyChangeSupport;
035    import java.io.BufferedInputStream;
036    import java.io.ByteArrayInputStream;
037    import java.io.ByteArrayOutputStream;
038    import java.io.IOException;
039    import java.io.InputStream;
040    import java.io.ObjectInputStream;
041    import java.io.ObjectOutputStream;
042    import java.io.Reader;
043    import java.io.Serializable;
044    import java.lang.ref.SoftReference;
045    import java.net.MalformedURLException;
046    import java.net.URI;
047    import java.net.URISyntaxException;
048    import java.net.URL;
049    import java.net.URLConnection;
050    import java.net.URLStreamHandler;
051    import java.util.HashMap;
052    import java.util.Iterator;
053    import java.util.zip.GZIPInputStream;
054    import javax.imageio.ImageIO;
055    import org.xml.sax.EntityResolver;
056    import org.xml.sax.InputSource;
057    import org.xml.sax.SAXException;
058    import org.xml.sax.SAXParseException;
059    import org.xml.sax.XMLReader;
060    import org.xml.sax.helpers.XMLReaderFactory;
061    
062    /**
063     * Many SVG files can be loaded at one time.  These files will quite likely
064     * need to reference one another.  The SVG universe provides a container for
065     * all these files and the means for them to relate to each other.
066     *
067     * @author Mark McKay
068     * @author <a href="mailto:mark@kitfox.com">Mark McKay</a>
069     */
070    public class SVGUniverse implements Serializable
071    {
072        public static final long serialVersionUID = 0;
073        
074        transient private PropertyChangeSupport changes = new PropertyChangeSupport(this);
075        
076        /**
077         * Maps document URIs to their loaded SVG diagrams.  Note that URIs for
078         * documents loaded from URLs will reflect their URLs and URIs for documents
079         * initiated from streams will have the scheme <i>svgSalamander</i>.
080         */
081        final HashMap loadedDocs = new HashMap();
082        
083        final HashMap loadedFonts = new HashMap();
084        
085        final HashMap loadedImages = new HashMap();
086        
087        public static final String INPUTSTREAM_SCHEME = "svgSalamander";
088        
089        /**
090         * Current time in this universe.  Used for resolving attributes that
091         * are influenced by track information.  Time is in milliseconds.  Time
092         * 0 coresponds to the time of 0 in each member diagram.
093         */
094        protected double curTime = 0.0;
095        
096        private boolean verbose = false;
097    
098        //Cache reader for efficiency
099        XMLReader cachedReader;
100        
101        /** Creates a new instance of SVGUniverse */
102        public SVGUniverse()
103        {
104        }
105        
106        public void addPropertyChangeListener(PropertyChangeListener l)
107        {
108            changes.addPropertyChangeListener(l);
109        }
110        
111        public void removePropertyChangeListener(PropertyChangeListener l)
112        {
113            changes.removePropertyChangeListener(l);
114        }
115        
116        /**
117         * Release all loaded SVG document from memory
118         */
119        public void clear()
120        {
121            loadedDocs.clear();
122            loadedFonts.clear();
123            loadedImages.clear();
124        }
125        
126        /**
127         * Returns the current animation time in milliseconds.
128         */
129        public double getCurTime()
130        { 
131            return curTime; 
132        }
133        
134        public void setCurTime(double curTime)
135        {
136            double oldTime = this.curTime;
137            this.curTime = curTime; 
138            changes.firePropertyChange("curTime", new Double(oldTime), new Double(curTime));
139        }
140        
141        /**
142         * Updates all time influenced style and presentation attributes in all SVG
143         * documents in this universe.
144         */
145        public void updateTime() throws SVGException
146        {
147            for (Iterator it = loadedDocs.values().iterator(); it.hasNext();)
148            {
149                SVGDiagram dia = (SVGDiagram)it.next();
150                dia.updateTime(curTime);
151            }
152        }
153        
154        /**
155         * Called by the Font element to let the universe know that a font has been
156         * loaded and is available.
157         */
158        void registerFont(Font font)
159        {
160            loadedFonts.put(font.getFontFace().getFontFamily(), font);
161        }
162        
163        public Font getDefaultFont()
164        {
165            for (Iterator it = loadedFonts.values().iterator(); it.hasNext();)
166            {
167                return (Font)it.next();
168            }
169            return null;
170        }
171        
172        public Font getFont(String fontName)
173        {
174            return (Font)loadedFonts.get(fontName);
175        }
176    
177        URL registerImage(URI imageURI)
178        {
179            String scheme = imageURI.getScheme();
180            if (scheme.equals("data"))
181            {
182                String path = imageURI.getRawSchemeSpecificPart();
183                int idx = path.indexOf(';');
184                String mime = path.substring(0, idx);
185                String content = path.substring(idx + 1);
186    
187                if (content.startsWith("base64"))
188                {
189                    content = content.substring(6);
190                    try {
191                        byte[] buf = new sun.misc.BASE64Decoder().decodeBuffer(content);
192                        ByteArrayInputStream bais = new ByteArrayInputStream(buf);
193                        BufferedImage img = ImageIO.read(bais);
194    
195                        URL url;
196                        int urlIdx = 0;
197                        while (true)
198                        {
199                            url = new URL("inlineImage", "localhost", "img" + urlIdx);
200                            if (!loadedImages.containsKey(url))
201                            {
202                                break;
203                            }
204                            urlIdx++;
205                        }
206    
207                        SoftReference ref = new SoftReference(img);
208                        loadedImages.put(url, ref);
209    
210                        return url;
211                    } catch (IOException ex) {
212                        ex.printStackTrace();
213                    }
214                }
215                return null;
216            }
217            else
218            {
219                try {
220                    URL url = imageURI.toURL();
221                    registerImage(url);
222                    return url;
223                } catch (MalformedURLException ex) {
224                    ex.printStackTrace();
225                }
226                return null;
227            }
228        }
229    
230        void registerImage(URL imageURL)
231        {
232            if (loadedImages.containsKey(imageURL)) return;
233            
234            SoftReference ref;
235            try
236            {
237                String fileName = imageURL.getFile();
238                if (".svg".equals(fileName.substring(fileName.length() - 4).toLowerCase()))
239                {
240                    SVGIcon icon = new SVGIcon();
241                    icon.setSvgURI(imageURL.toURI());
242                    
243                    BufferedImage img = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
244                    Graphics2D g = img.createGraphics();
245                    icon.paintIcon(null, g, 0, 0);
246                    g.dispose();
247                    ref = new SoftReference(img);
248                }
249                else
250                {
251                    BufferedImage img = ImageIO.read(imageURL);
252                    ref = new SoftReference(img);
253                }
254                loadedImages.put(imageURL, ref);
255            }
256            catch (Exception e)
257            {
258                System.err.println("Could not load image: " + imageURL);
259                e.printStackTrace();
260            }
261        }
262        
263        BufferedImage getImage(URL imageURL)
264        {
265            SoftReference ref = (SoftReference)loadedImages.get(imageURL);
266            if (ref == null) return null;
267            
268            BufferedImage img = (BufferedImage)ref.get();
269            //If image was cleared from memory, reload it
270            if (img == null)
271            {
272                try
273                {
274                    img = ImageIO.read(imageURL);
275                }
276                catch (Exception e)
277                { e.printStackTrace(); }
278                ref = new SoftReference(img);
279                loadedImages.put(imageURL, ref);
280            }
281            
282            return img;
283        }
284        
285        /**
286         * Returns the element of the document at the given URI.  If the document
287         * is not already loaded, it will be.
288         */
289        public SVGElement getElement(URI path)
290        {
291            return getElement(path, true);
292        }
293        
294        public SVGElement getElement(URL path)
295        {
296            try
297            {
298                URI uri = new URI(path.toString());
299                return getElement(uri, true);
300            }
301            catch (Exception e)
302            {
303                e.printStackTrace();
304            }
305            return null;
306        }
307        
308        /**
309         * Looks up a href within our universe.  If the href refers to a document that
310         * is not loaded, it will be loaded.  The URL #target will then be checked
311         * against the SVG diagram's index and the coresponding element returned.
312         * If there is no coresponding index, null is returned.
313         */
314        public SVGElement getElement(URI path, boolean loadIfAbsent)
315        {
316            try
317            {
318                //Strip fragment from URI
319                URI xmlBase = new URI(path.getScheme(), path.getSchemeSpecificPart(), null);
320                
321                SVGDiagram dia = (SVGDiagram)loadedDocs.get(xmlBase);
322                if (dia == null && loadIfAbsent)
323                {
324    //System.err.println("SVGUnivserse: " + xmlBase.toString());
325    //javax.swing.JOptionPane.showMessageDialog(null, xmlBase.toString());
326                    URL url = xmlBase.toURL();
327                    
328                    loadSVG(url, false);
329                    dia = (SVGDiagram)loadedDocs.get(xmlBase);
330                    if (dia == null) return null;
331                }
332                
333                String fragment = path.getFragment();
334                return fragment == null ? dia.getRoot() : dia.getElement(fragment);
335            }
336            catch (Exception e)
337            {
338                e.printStackTrace();
339                return null;
340            }
341        }
342        
343        public SVGDiagram getDiagram(URI xmlBase)
344        {
345            return getDiagram(xmlBase, true);
346        }
347        
348        /**
349         * Returns the diagram that has been loaded from this root.  If diagram is
350         * not already loaded, returns null.
351         */
352        public SVGDiagram getDiagram(URI xmlBase, boolean loadIfAbsent)
353        {
354            if (xmlBase == null) return null;
355    
356            SVGDiagram dia = (SVGDiagram)loadedDocs.get(xmlBase);
357            if (dia != null || !loadIfAbsent) return dia;
358            
359            //Load missing diagram
360            try
361            {
362                URL url;
363                if ("jar".equals(xmlBase.getScheme()) && xmlBase.getPath() != null && !xmlBase.getPath().contains("!/"))
364                {
365                    //Workaround for resources stored in jars loaded by Webstart.
366                    //http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6753651
367                    url = SVGUniverse.class.getResource("xmlBase.getPath()");
368                }
369                else
370                {
371                    url = xmlBase.toURL();
372                }
373    
374                
375                loadSVG(url, false);
376                dia = (SVGDiagram)loadedDocs.get(xmlBase);
377                return dia;
378            }
379            catch (Exception e)
380            {
381                e.printStackTrace();
382            }
383            
384            return null;
385        }
386        
387        /**
388         * Wraps input stream in a BufferedInputStream.  If it is detected that this
389         * input stream is GZIPped, also wraps in a GZIPInputStream for inflation.
390         * 
391         * @param is Raw input stream
392         * @return Uncompressed stream of SVG data
393         * @throws java.io.IOException
394         */
395        private InputStream createDocumentInputStream(InputStream is) throws IOException
396        {
397            BufferedInputStream bin = new BufferedInputStream(is);
398            bin.mark(2);
399            int b0 = bin.read();
400            int b1 = bin.read();
401            bin.reset();
402    
403            //Check for gzip magic number
404            if ((b1 << 8 | b0) == GZIPInputStream.GZIP_MAGIC)
405            {
406                GZIPInputStream iis = new GZIPInputStream(bin);
407                return iis;
408            }
409            else
410            {
411                //Plain text
412                return bin;
413            }
414        }
415        
416        public URI loadSVG(URL docRoot)
417        {
418            return loadSVG(docRoot, false);
419        }
420        
421        /**
422         * Loads an SVG file and all the files it references from the URL provided.
423         * If a referenced file already exists in the SVG universe, it is not
424         * reloaded.
425         * @param docRoot - URL to the location where this SVG file can be found.
426         * @param forceLoad - if true, ignore cached diagram and reload
427         * @return - The URI that refers to the loaded document
428         */
429        public URI loadSVG(URL docRoot, boolean forceLoad)
430        {
431            try
432            {
433                URI uri = new URI(docRoot.toString());
434                if (loadedDocs.containsKey(uri) && !forceLoad) return uri;
435                
436                InputStream is = docRoot.openStream();
437                return loadSVG(uri, new InputSource(createDocumentInputStream(is)));
438            }
439            catch (URISyntaxException ex)
440            {
441                ex.printStackTrace();
442            }
443            catch (IOException ex)
444            {
445                ex.printStackTrace();
446            }
447            
448            return null;
449        }
450        
451        
452        public URI loadSVG(InputStream is, String name) throws IOException
453        {
454            return loadSVG(is, name, false);
455        }
456        
457        public URI loadSVG(InputStream is, String name, boolean forceLoad) throws IOException
458        {
459            URI uri = getStreamBuiltURI(name);
460            if (uri == null) return null;
461            if (loadedDocs.containsKey(uri) && !forceLoad) return uri;
462            
463            return loadSVG(uri, new InputSource(createDocumentInputStream(is)));
464        }
465        
466        public URI loadSVG(Reader reader, String name)
467        {
468            return loadSVG(reader, name, false);
469        }
470        
471        /**
472         * This routine allows you to create SVG documents from data streams that
473         * may not necessarily have a URL to load from.  Since every SVG document
474         * must be identified by a unique URL, Salamander provides a method to
475         * fake this for streams by defining it's own protocol - svgSalamander -
476         * for SVG documents without a formal URL.
477         *
478         * @param reader - A stream containing a valid SVG document
479         * @param name - <p>A unique name for this document.  It will be used to
480         * construct a unique URI to refer to this document and perform resolution
481         * with relative URIs within this document.</p>
482         * <p>For example, a name of "/myScene" will produce the URI
483         * svgSalamander:/myScene.  "/maps/canada/toronto" will produce
484         * svgSalamander:/maps/canada/toronto.  If this second document then
485         * contained the href "../uk/london", it would resolve by default to
486         * svgSalamander:/maps/uk/london.  That is, SVG Salamander defines the
487         * URI scheme svgSalamander for it's own internal use and uses it
488         * for uniquely identfying documents loaded by stream.</p>
489         * <p>If you need to link to documents outside of this scheme, you can
490         * either supply full hrefs (eg, href="url(http://www.kitfox.com/index.html)")
491         * or put the xml:base attribute in a tag to change the defaultbase
492         * URIs are resolved against</p>
493         * <p>If a name does not start with the character '/', it will be automatically
494         * prefixed to it.</p>
495         * @param forceLoad - if true, ignore cached diagram and reload
496         *
497         * @return - The URI that refers to the loaded document
498         */
499        public URI loadSVG(Reader reader, String name, boolean forceLoad)
500        {
501    //System.err.println(url.toString());
502            //Synthesize URI for this stream
503            URI uri = getStreamBuiltURI(name);
504            if (uri == null) return null;
505            if (loadedDocs.containsKey(uri) && !forceLoad) return uri;
506            
507            return loadSVG(uri, new InputSource(reader));
508        }
509        
510        /**
511         * Synthesize a URI for an SVGDiagram constructed from a stream.
512         * @param name - Name given the document constructed from a stream.
513         */
514        public URI getStreamBuiltURI(String name)
515        {
516            if (name == null || name.length() == 0) return null;
517            
518            if (name.charAt(0) != '/') name = '/' + name;
519            
520            try
521            {
522                //Dummy URL for SVG documents built from image streams
523                return new URI(INPUTSTREAM_SCHEME, name, null);
524            }
525            catch (Exception e)
526            {
527                e.printStackTrace();
528                return null;
529            }
530        }
531        
532        private XMLReader getXMLReaderCached() throws SAXException
533        {
534            if (cachedReader == null)
535            {
536                cachedReader = XMLReaderFactory.createXMLReader();
537            }
538            return cachedReader;
539        }
540        
541    //    protected URI loadSVG(URI xmlBase, InputStream is)
542        protected URI loadSVG(URI xmlBase, InputSource is)
543        {
544            // Use an instance of ourselves as the SAX event handler
545            SVGLoader handler = new SVGLoader(xmlBase, this, verbose);
546            
547            //Place this docment in the universe before it is completely loaded
548            // so that the load process can refer to references within it's current
549            // document
550    //System.err.println("SVGUniverse: loading dia " + xmlBase);
551            loadedDocs.put(xmlBase, handler.getLoadedDiagram());
552            
553            // Use the default (non-validating) parser
554    //        SAXParserFactory factory = SAXParserFactory.newInstance();
555    //        factory.setValidating(false);
556    //        factory.setNamespaceAware(true);
557            
558            try
559            {
560                // Parse the input
561                XMLReader reader = getXMLReaderCached();
562                reader.setEntityResolver(
563                    new EntityResolver()
564                    {
565                        public InputSource resolveEntity(String publicId, String systemId)
566                        {
567                            //Ignore all DTDs
568                            return new InputSource(new ByteArrayInputStream(new byte[0]));
569                        }
570                    }
571                );
572                reader.setContentHandler(handler);
573                reader.parse(is);
574                
575    //            SAXParser saxParser = factory.newSAXParser();
576    //            saxParser.parse(new InputSource(new BufferedReader(is)), handler);
577                return xmlBase;
578            }
579            catch (SAXParseException sex)
580            {
581                System.err.println("Error processing " + xmlBase);
582                System.err.println(sex.getMessage());
583                
584                loadedDocs.remove(xmlBase);
585                return null;
586            }
587            catch (Throwable t)
588            {
589                t.printStackTrace();
590            }
591            
592            return null;
593        }
594        
595        public static void main(String argv[])
596        {
597            try
598            {
599                URL url = new URL("svgSalamander", "localhost", -1, "abc.svg",
600                        new URLStreamHandler()
601                {
602                    protected URLConnection openConnection(URL u)
603                    {
604                        return null;
605                    }
606                }
607                );
608    //            URL url2 = new URL("svgSalamander", "localhost", -1, "abc.svg");
609                
610                //Investigate URI resolution
611                URI uriA, uriB, uriC, uriD, uriE;
612                
613                uriA = new URI("svgSalamander", "/names/mySpecialName", null);
614    //            uriA = new URI("http://www.kitfox.com/salamander");
615    //            uriA = new URI("svgSalamander://mySpecialName/grape");
616                System.err.println(uriA.toString());
617                System.err.println(uriA.getScheme());
618                
619                uriB = uriA.resolve("#begin");
620                System.err.println(uriB.toString());
621                
622                uriC = uriA.resolve("tree#boing");
623                System.err.println(uriC.toString());
624                
625                uriC = uriA.resolve("../tree#boing");
626                System.err.println(uriC.toString());
627            }
628            catch (Exception e)
629            {
630                e.printStackTrace();
631            }
632        }
633    
634        public boolean isVerbose()
635        {
636            return verbose;
637        }
638    
639        public void setVerbose(boolean verbose)
640        {
641            this.verbose = verbose;
642        }
643        
644        /**
645         * Uses serialization to duplicate this universe.
646         */
647        public SVGUniverse duplicate() throws IOException, ClassNotFoundException
648        {
649            ByteArrayOutputStream bs = new ByteArrayOutputStream();
650            ObjectOutputStream os = new ObjectOutputStream(bs);
651            os.writeObject(this);
652            os.close();
653    
654            ByteArrayInputStream bin = new ByteArrayInputStream(bs.toByteArray());
655            ObjectInputStream is = new ObjectInputStream(bin);
656            SVGUniverse universe = (SVGUniverse)is.readObject();
657            is.close();
658    
659            return universe;
660        }
661    }