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