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.logging.Level;
061import java.util.logging.Logger;
062import java.util.zip.GZIPInputStream;
063import javax.imageio.ImageIO;
064import javax.xml.parsers.ParserConfigurationException;
065import javax.xml.parsers.SAXParserFactory;
066import org.xml.sax.EntityResolver;
067import org.xml.sax.InputSource;
068import org.xml.sax.SAXException;
069import org.xml.sax.SAXParseException;
070import org.xml.sax.XMLReader;
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<URI, SVGDiagram> loadedDocs = new HashMap<URI, SVGDiagram>();
091    final HashMap<String, Font> loadedFonts = new HashMap<String, Font>();
092    final HashMap<URL, SoftReference<BufferedImage>> loadedImages = new HashMap<URL, SoftReference<BufferedImage>>();
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
102    //If true, <imageSVG> elements will only load image data that is included using inline data: uris
103    private boolean imageDataInlineOnly = false;
104    
105    /**
106     * Creates a new instance of SVGUniverse
107     */
108    public SVGUniverse()
109    {
110    }
111
112    public void addPropertyChangeListener(PropertyChangeListener l)
113    {
114        changes.addPropertyChangeListener(l);
115    }
116
117    public void removePropertyChangeListener(PropertyChangeListener l)
118    {
119        changes.removePropertyChangeListener(l);
120    }
121
122    /**
123     * Release all loaded SVG document from memory
124     */
125    public void clear()
126    {
127        loadedDocs.clear();
128        loadedFonts.clear();
129        loadedImages.clear();
130    }
131
132    /**
133     * Returns the current animation time in milliseconds.
134     */
135    public double getCurTime()
136    {
137        return curTime;
138    }
139
140    public void setCurTime(double curTime)
141    {
142        double oldTime = this.curTime;
143        this.curTime = curTime;
144        changes.firePropertyChange("curTime", new Double(oldTime), new Double(curTime));
145    }
146
147    /**
148     * Updates all time influenced style and presentation attributes in all SVG
149     * documents in this universe.
150     */
151    public void updateTime() throws SVGException
152    {
153        for (SVGDiagram dia : loadedDocs.values()) {
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 (Font font : loadedFonts.values()) {
170            return font;
171        }
172        return null;
173    }
174
175    public Font getFont(String fontName)
176    {
177        return (Font) loadedFonts.get(fontName);
178    }
179
180    URL registerImage(URI imageURI)
181    {
182        String scheme = imageURI.getScheme();
183        if (scheme.equals("data"))
184        {
185            String path = imageURI.getRawSchemeSpecificPart();
186            int idx = path.indexOf(';');
187            String mime = path.substring(0, idx);
188            String content = path.substring(idx + 1);
189
190            if (content.startsWith("base64"))
191            {
192                content = content.substring(6);
193                try
194                {
195//                    byte[] buf = new sun.misc.BASE64Decoder().decodeBuffer(content);
196//                    ByteArrayInputStream bais = new ByteArrayInputStream(buf);
197                    ByteArrayInputStream bis = new ByteArrayInputStream(content.getBytes());
198                    Base64InputStream bais = new Base64InputStream(bis);
199                    
200                    BufferedImage img = ImageIO.read(bais);
201
202                    URL url;
203                    int urlIdx = 0;
204                    while (true)
205                    {
206                        url = new URL("inlineImage", "localhost", "img" + urlIdx);
207                        if (!loadedImages.containsKey(url))
208                        {
209                            break;
210                        }
211                        urlIdx++;
212                    }
213
214                    SoftReference<BufferedImage> ref = new SoftReference<BufferedImage>(img);
215                    loadedImages.put(url, ref);
216
217                    return url;
218                } catch (IOException ex)
219                {
220                    Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
221                        "Could not decode inline image", ex);
222                }
223            }
224            return null;
225        } else
226        {
227            try
228            {
229                URL url = imageURI.toURL();
230                registerImage(url);
231                return url;
232            } catch (MalformedURLException ex)
233            {
234                Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
235                    "Bad url", ex);
236            }
237            return null;
238        }
239    }
240
241    void registerImage(URL imageURL)
242    {
243        if (loadedImages.containsKey(imageURL))
244        {
245            return;
246        }
247
248        SoftReference<BufferedImage> ref;
249        try
250        {
251            String fileName = imageURL.getFile();
252            if (".svg".equals(fileName.substring(fileName.length() - 4).toLowerCase()))
253            {
254                SVGIcon icon = new SVGIcon();
255                icon.setSvgURI(imageURL.toURI());
256
257                BufferedImage img = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
258                Graphics2D g = img.createGraphics();
259                icon.paintIcon(null, g, 0, 0);
260                g.dispose();
261                ref = new SoftReference<BufferedImage>(img);
262            } else
263            {
264                BufferedImage img = ImageIO.read(imageURL);
265                ref = new SoftReference<BufferedImage>(img);
266            }
267            loadedImages.put(imageURL, ref);
268        } catch (Exception e)
269        {
270            Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
271                "Could not load image: " + imageURL, e);
272        }
273    }
274
275    BufferedImage getImage(URL imageURL)
276    {
277        SoftReference<BufferedImage> ref = (SoftReference<BufferedImage>) loadedImages.get(imageURL);
278        if (ref == null)
279        {
280            return null;
281        }
282
283        BufferedImage img = (BufferedImage) ref.get();
284        //If image was cleared from memory, reload it
285        if (img == null)
286        {
287            try
288            {
289                img = ImageIO.read(imageURL);
290            } catch (Exception e)
291            {
292                Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
293                    "Could not load image", e);
294            }
295            ref = new SoftReference<BufferedImage>(img);
296            loadedImages.put(imageURL, ref);
297        }
298
299        return img;
300    }
301
302    /**
303     * Returns the element of the document at the given URI. If the document is
304     * not already loaded, it will be.
305     */
306    public SVGElement getElement(URI path)
307    {
308        return getElement(path, true);
309    }
310
311    public SVGElement getElement(URL path)
312    {
313        try
314        {
315            URI uri = new URI(path.toString());
316            return getElement(uri, true);
317        } catch (Exception e)
318        {
319            Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
320                "Could not parse url " + path, e);
321        }
322        return null;
323    }
324
325    /**
326     * Looks up a href within our universe. If the href refers to a document
327     * that is not loaded, it will be loaded. The URL #target will then be
328     * checked against the SVG diagram's index and the coresponding element
329     * returned. If there is no coresponding index, null is returned.
330     */
331    public SVGElement getElement(URI path, boolean loadIfAbsent)
332    {
333        try
334        {
335            //Strip fragment from URI
336            URI xmlBase = new URI(path.getScheme(), path.getSchemeSpecificPart(), null);
337
338            SVGDiagram dia = (SVGDiagram) loadedDocs.get(xmlBase);
339            if (dia == null && loadIfAbsent)
340            {
341//System.err.println("SVGUnivserse: " + xmlBase.toString());
342//javax.swing.JOptionPane.showMessageDialog(null, xmlBase.toString());
343                URL url = xmlBase.toURL();
344
345                loadSVG(url, false);
346                dia = (SVGDiagram) loadedDocs.get(xmlBase);
347                if (dia == null)
348                {
349                    return null;
350                }
351            }
352
353            String fragment = path.getFragment();
354            return fragment == null ? dia.getRoot() : dia.getElement(fragment);
355        } catch (Exception e)
356        {
357            Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
358                "Could not parse path " + path, e);
359            return null;
360        }
361    }
362
363    public SVGDiagram getDiagram(URI xmlBase)
364    {
365        return getDiagram(xmlBase, true);
366    }
367
368    /**
369     * Returns the diagram that has been loaded from this root. If diagram is
370     * not already loaded, returns null.
371     */
372    public SVGDiagram getDiagram(URI xmlBase, boolean loadIfAbsent)
373    {
374        if (xmlBase == null)
375        {
376            return null;
377        }
378
379        SVGDiagram dia = (SVGDiagram) loadedDocs.get(xmlBase);
380        if (dia != null || !loadIfAbsent)
381        {
382            return dia;
383        }
384
385        //Load missing diagram
386        try
387        {
388            URL url;
389            if ("jar".equals(xmlBase.getScheme()) && xmlBase.getPath() != null && !xmlBase.getPath().contains("!/"))
390            {
391                //Workaround for resources stored in jars loaded by Webstart.
392                //http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6753651
393                url = SVGUniverse.class.getResource("xmlBase.getPath()");
394            }
395            else
396            {
397                url = xmlBase.toURL();
398            }
399
400
401            loadSVG(url, false);
402            dia = (SVGDiagram) loadedDocs.get(xmlBase);
403            return dia;
404        } catch (Exception e)
405        {
406            Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
407                "Could not parse", e);
408        }
409
410        return null;
411    }
412
413    /**
414     * Wraps input stream in a BufferedInputStream. If it is detected that this
415     * input stream is GZIPped, also wraps in a GZIPInputStream for inflation.
416     *
417     * @param is Raw input stream
418     * @return Uncompressed stream of SVG data
419     * @throws java.io.IOException
420     */
421    private InputStream createDocumentInputStream(InputStream is) throws IOException
422    {
423        BufferedInputStream bin = new BufferedInputStream(is);
424        bin.mark(2);
425        int b0 = bin.read();
426        int b1 = bin.read();
427        bin.reset();
428
429        //Check for gzip magic number
430        if ((b1 << 8 | b0) == GZIPInputStream.GZIP_MAGIC)
431        {
432            GZIPInputStream iis = new GZIPInputStream(bin);
433            return iis;
434        } else
435        {
436            //Plain text
437            return bin;
438        }
439    }
440
441    public URI loadSVG(URL docRoot)
442    {
443        return loadSVG(docRoot, false);
444    }
445
446    /**
447     * Loads an SVG file and all the files it references from the URL provided.
448     * If a referenced file already exists in the SVG universe, it is not
449     * reloaded.
450     *
451     * @param docRoot - URL to the location where this SVG file can be found.
452     * @param forceLoad - if true, ignore cached diagram and reload
453     * @return - The URI that refers to the loaded document
454     */
455    public URI loadSVG(URL docRoot, boolean forceLoad)
456    {
457        try
458        {
459            URI uri = new URI(docRoot.toString());
460            if (loadedDocs.containsKey(uri) && !forceLoad)
461            {
462                return uri;
463            }
464
465            InputStream is = docRoot.openStream();
466            return loadSVG(uri, new InputSource(createDocumentInputStream(is)));
467        } catch (URISyntaxException ex)
468        {
469            Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
470                "Could not parse", ex);
471        } catch (IOException e)
472        {
473            Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
474                "Could not parse", e);
475        }
476
477        return null;
478    }
479
480    public URI loadSVG(InputStream is, String name) throws IOException
481    {
482        return loadSVG(is, name, false);
483    }
484
485    public URI loadSVG(InputStream is, String name, boolean forceLoad) throws IOException
486    {
487        URI uri = getStreamBuiltURI(name);
488        if (uri == null)
489        {
490            return null;
491        }
492        if (loadedDocs.containsKey(uri) && !forceLoad)
493        {
494            return uri;
495        }
496
497        return loadSVG(uri, new InputSource(createDocumentInputStream(is)));
498    }
499
500    public URI loadSVG(Reader reader, String name)
501    {
502        return loadSVG(reader, name, false);
503    }
504
505    /**
506     * This routine allows you to create SVG documents from data streams that
507     * may not necessarily have a URL to load from. Since every SVG document
508     * must be identified by a unique URL, Salamander provides a method to fake
509     * this for streams by defining it's own protocol - svgSalamander - for SVG
510     * documents without a formal URL.
511     *
512     * @param reader - A stream containing a valid SVG document
513     * @param name - <p>A unique name for this document. It will be used to
514     * construct a unique URI to refer to this document and perform resolution
515     * with relative URIs within this document.</p> <p>For example, a name of
516     * "/myScene" will produce the URI svgSalamander:/myScene.
517     * "/maps/canada/toronto" will produce svgSalamander:/maps/canada/toronto.
518     * If this second document then contained the href "../uk/london", it would
519     * resolve by default to svgSalamander:/maps/uk/london. That is, SVG
520     * Salamander defines the URI scheme svgSalamander for it's own internal use
521     * and uses it for uniquely identfying documents loaded by stream.</p> <p>If
522     * you need to link to documents outside of this scheme, you can either
523     * supply full hrefs (eg, href="url(http://www.kitfox.com/index.html)") or
524     * put the xml:base attribute in a tag to change the defaultbase URIs are
525     * resolved against</p> <p>If a name does not start with the character '/',
526     * it will be automatically prefixed to it.</p>
527     * @param forceLoad - if true, ignore cached diagram and reload
528     *
529     * @return - The URI that refers to the loaded document
530     */
531    public URI loadSVG(Reader reader, String name, boolean forceLoad)
532    {
533//System.err.println(url.toString());
534        //Synthesize URI for this stream
535        URI uri = getStreamBuiltURI(name);
536        if (uri == null)
537        {
538            return null;
539        }
540        if (loadedDocs.containsKey(uri) && !forceLoad)
541        {
542            return uri;
543        }
544
545        return loadSVG(uri, new InputSource(reader));
546    }
547
548    /**
549     * Synthesize a URI for an SVGDiagram constructed from a stream.
550     *
551     * @param name - Name given the document constructed from a stream.
552     */
553    public URI getStreamBuiltURI(String name)
554    {
555        if (name == null || name.length() == 0)
556        {
557            return null;
558        }
559
560        if (name.charAt(0) != '/')
561        {
562            name = '/' + name;
563        }
564
565        try
566        {
567            //Dummy URL for SVG documents built from image streams
568            return new URI(INPUTSTREAM_SCHEME, name, null);
569        } catch (Exception e)
570        {
571            Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
572                "Could not parse", e);
573            return null;
574        }
575    }
576
577    private XMLReader getXMLReader() throws SAXException, ParserConfigurationException
578    {
579        SAXParserFactory factory = SAXParserFactory.newInstance();
580        factory.setNamespaceAware(true);
581        return factory.newSAXParser().getXMLReader();
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 = getXMLReader();
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            Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
615                "Error processing " + xmlBase, sex);
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<URI> getLoadedDocumentURIs()
633    {
634        return new ArrayList<URI>(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
674    /**
675     * @return the imageDataInlineOnly
676     */
677    public boolean isImageDataInlineOnly()
678    {
679        return imageDataInlineOnly;
680    }
681
682    /**
683     * @param imageDataInlineOnly the imageDataInlineOnly to set
684     */
685    public void setImageDataInlineOnly(boolean imageDataInlineOnly)
686    {
687        this.imageDataInlineOnly = imageDataInlineOnly;
688    }
689}