source: josm/trunk/src/com/kitfox/svg/SVGUniverse.java@ 10741

Last change on this file since 10741 was 10741, checked in by Don-vip, 8 years ago

see #11924 - XMLReaderFactory has been deprecated in Java 9, see https://bugs.openjdk.java.net/browse/JDK-8152912

  • Property svn:eol-style set to native
File size: 21.0 KB
Line 
1/*
2 * SVG Salamander
3 * Copyright (c) 2004, Mark McKay
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or
7 * without modification, are permitted provided that the following
8 * conditions are met:
9 *
10 * - Redistributions of source code must retain the above
11 * copyright notice, this list of conditions and the following
12 * disclaimer.
13 * - Redistributions in binary form must reproduce the above
14 * copyright notice, this list of conditions and the following
15 * disclaimer in the documentation and/or other materials
16 * provided with the distribution.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
23 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
27 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
29 * OF THE POSSIBILITY OF SUCH DAMAGE.
30 *
31 * Mark McKay can be contacted at mark@kitfox.com. Salamander and other
32 * projects can be found at http://www.kitfox.com
33 *
34 * Created on February 18, 2004, 11:43 PM
35 */
36package com.kitfox.svg;
37
38import com.kitfox.svg.app.beans.SVGIcon;
39import java.awt.Graphics2D;
40import java.awt.image.BufferedImage;
41import java.beans.PropertyChangeListener;
42import java.beans.PropertyChangeSupport;
43import java.io.BufferedInputStream;
44import java.io.ByteArrayInputStream;
45import java.io.ByteArrayOutputStream;
46import java.io.IOException;
47import java.io.InputStream;
48import java.io.ObjectInputStream;
49import java.io.ObjectOutputStream;
50import java.io.Reader;
51import java.io.Serializable;
52import java.lang.ref.SoftReference;
53import java.net.MalformedURLException;
54import java.net.URI;
55import java.net.URISyntaxException;
56import java.net.URL;
57import java.util.ArrayList;
58import java.util.HashMap;
59import java.util.Iterator;
60import java.util.logging.Level;
61import java.util.logging.Logger;
62import java.util.zip.GZIPInputStream;
63import javax.imageio.ImageIO;
64import javax.xml.bind.DatatypeConverter;
65import javax.xml.parsers.ParserConfigurationException;
66import javax.xml.parsers.SAXParserFactory;
67import org.xml.sax.EntityResolver;
68import org.xml.sax.InputSource;
69import org.xml.sax.SAXException;
70import org.xml.sax.SAXParseException;
71import org.xml.sax.XMLReader;
72
73/**
74 * Many SVG files can be loaded at one time. These files will quite likely need
75 * to reference one another. The SVG universe provides a container for all these
76 * files and the means for them to relate to each other.
77 *
78 * @author Mark McKay
79 * @author <a href="mailto:mark@kitfox.com">Mark McKay</a>
80 */
81public class SVGUniverse implements Serializable
82{
83
84 public static final long serialVersionUID = 0;
85 transient private PropertyChangeSupport changes = new PropertyChangeSupport(this);
86 /**
87 * Maps document URIs to their loaded SVG diagrams. Note that URIs for
88 * documents loaded from URLs will reflect their URLs and URIs for documents
89 * initiated from streams will have the scheme <i>svgSalamander</i>.
90 */
91 final HashMap loadedDocs = new HashMap();
92 final HashMap loadedFonts = new HashMap();
93 final HashMap loadedImages = new HashMap();
94 public static final String INPUTSTREAM_SCHEME = "svgSalamander";
95 /**
96 * Current time in this universe. Used for resolving attributes that are
97 * influenced by track information. Time is in milliseconds. Time 0
98 * coresponds to the time of 0 in each member diagram.
99 */
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 try
196 {
197 byte[] buf = DatatypeConverter.parseBase64Binary(content.substring(6));
198 ByteArrayInputStream bais = new ByteArrayInputStream(buf);
199 BufferedImage img = ImageIO.read(bais);
200
201 URL url;
202 int urlIdx = 0;
203 while (true)
204 {
205 url = new URL("inlineImage", "localhost", "img" + urlIdx);
206 if (!loadedImages.containsKey(url))
207 {
208 break;
209 }
210 urlIdx++;
211 }
212
213 SoftReference ref = new SoftReference(img);
214 loadedImages.put(url, ref);
215
216 return url;
217 }
218 catch (IOException | IllegalArgumentException 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 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(img);
262 } else
263 {
264 BufferedImage img = ImageIO.read(imageURL);
265 ref = new SoftReference(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 ref = (SoftReference) 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(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 getXMLReaderCached() throws SAXException, ParserConfigurationException
578 {
579 if (cachedReader == null)
580 {
581 SAXParserFactory factory = SAXParserFactory.newInstance();
582 factory.setNamespaceAware(true);
583 cachedReader = factory.newSAXParser().getXMLReader();
584 }
585 return cachedReader;
586 }
587
588 protected URI loadSVG(URI xmlBase, InputSource is)
589 {
590 // Use an instance of ourselves as the SAX event handler
591 SVGLoader handler = new SVGLoader(xmlBase, this, verbose);
592
593 //Place this docment in the universe before it is completely loaded
594 // so that the load process can refer to references within it's current
595 // document
596 loadedDocs.put(xmlBase, handler.getLoadedDiagram());
597
598 try
599 {
600 // Parse the input
601 XMLReader reader = getXMLReaderCached();
602 reader.setEntityResolver(
603 new EntityResolver()
604 {
605 public InputSource resolveEntity(String publicId, String systemId)
606 {
607 //Ignore all DTDs
608 return new InputSource(new ByteArrayInputStream(new byte[0]));
609 }
610 });
611 reader.setContentHandler(handler);
612 reader.parse(is);
613
614 handler.getLoadedDiagram().updateTime(curTime);
615 return xmlBase;
616 } catch (SAXParseException sex)
617 {
618 System.err.println("Error processing " + xmlBase);
619 System.err.println(sex.getMessage());
620
621 loadedDocs.remove(xmlBase);
622 return null;
623 } catch (Throwable e)
624 {
625 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
626 "Could not load SVG " + xmlBase, e);
627 }
628
629 return null;
630 }
631
632 /**
633 * Get list of uris of all loaded documents and subdocuments.
634 * @return
635 */
636 public ArrayList getLoadedDocumentURIs()
637 {
638 return new ArrayList(loadedDocs.keySet());
639 }
640
641 /**
642 * Remove loaded document from cache.
643 * @param uri
644 */
645 public void removeDocument(URI uri)
646 {
647 loadedDocs.remove(uri);
648 }
649
650 public boolean isVerbose()
651 {
652 return verbose;
653 }
654
655 public void setVerbose(boolean verbose)
656 {
657 this.verbose = verbose;
658 }
659
660 /**
661 * Uses serialization to duplicate this universe.
662 */
663 public SVGUniverse duplicate() throws IOException, ClassNotFoundException
664 {
665 ByteArrayOutputStream bs = new ByteArrayOutputStream();
666 ObjectOutputStream os = new ObjectOutputStream(bs);
667 os.writeObject(this);
668 os.close();
669
670 ByteArrayInputStream bin = new ByteArrayInputStream(bs.toByteArray());
671 ObjectInputStream is = new ObjectInputStream(bin);
672 SVGUniverse universe = (SVGUniverse) is.readObject();
673 is.close();
674
675 return universe;
676 }
677}
Note: See TracBrowser for help on using the repository browser.