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