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