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