/* * $Id$ * * Copyright (c) 2008 Brent Easton * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License (LGPL) as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, copies are available * at http://www.opensource.org. */ package VASSAL.tools.icon; import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.JarURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.jar.JarEntry; import java.util.jar.JarFile; import javax.swing.Icon; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import VASSAL.build.IllegalBuildException; import VASSAL.i18n.Resources; import VASSAL.tools.DataArchive; import VASSAL.tools.ErrorDialog; import VASSAL.tools.JarArchive; import VASSAL.tools.ReadErrorDialog; import VASSAL.tools.io.IOUtils; /** * Manage and Provide Icons in standard sizes. * */ public final class IconFactory { private static final Logger logger = LoggerFactory.getLogger(IconFactory.class); static final String FILE = "file:"; //$NON-NLS-1$ static final String JAR = "jar:"; //$NON-NLS-1$ private static final JarArchive jar = new JarArchive(); private static IconFactory instance = new IconFactory(); private Map<String, IconFamily> iconFamilies = new ConcurrentHashMap<String, IconFamily>(); private static final Object preloadLock = new Object(); private Thread preloadThread; /** * Set the Singleton instance * @param i */ static void setInstance(IconFactory i) { instance = i; } /** * Return the Singleton instance. */ static IconFactory getInstance() { if (instance == null) { throw new IllegalStateException("No IconFactory instance"); //$NON-NLS-1$ } return instance; } /** * Create a new IconFactory. */ public IconFactory () { // FIXME: Maybe send this off to an executor? // FIXME: preloadThread is never set to null, cannot be gc'd // Find all available Icon Familys within Vassal. // May take a little while, so run it on a background thread preloadThread = new Thread(new Runnable(){ public void run() { synchronized (preloadLock) { try { initVassalIconFamilys(); } catch (IllegalBuildException e) { ErrorDialog.bug(e); } } } }, "IconFactory-preload"); //$NON-NLS-1$ preloadThread.start(); } /** * Return an Icon of the specified size. Standard sizes are defined in IconFamily * * @param IconFamilyName Name of Icon family * @param size Size (See IconFamily) * @return Sized Icon */ public static Icon getIcon(String iconFamilyName, int size) { IconFamily family = getInstance().getFamily(iconFamilyName); if (family == null) { throw new IllegalStateException(Resources.getString("Error.not_found", IconFamily.getConfigureTypeName() + " " + iconFamilyName)); //$NON-NLS-1$ } return family.getIcon(size); } /** Return an Icon of the specified size as an Image. Standard sizes are defined in IconFamily * * @param IconFamilyName Name of Icon family * @param size Size (See IconFamily) * @return Sized Image */ public static BufferedImage getImage(String iconFamilyName, int size) { final IconFamily family = getInstance().getFamily(iconFamilyName); if (family == null) { throw new IllegalStateException(Resources.getString("Error.not_found", IconFamily.getConfigureTypeName()+ " " + iconFamilyName)); //$NON-NLS-1$ } return family.getImage(size); } /** * Return a sorted list of all Icon Family names. * @return */ public static List<String> getIconFamilyNames() { return getInstance().getIconFamilyList(); } /** * Add a new IconFamily * @param family Icon Family Name */ public static void addIconFamily(IconFamily family) { getInstance().add(family); } /** * Remove an IconFamily * @param name Icon Family Name */ public static void removeIconFamily(IconFamily family) { getInstance().remove(family); } /** * Rename an IconFamily * * @param oldName Old Icon Family Name * @param newName New Icon Family Name */ public static void renameIconFamily(String oldName, IconFamily family) { getInstance().rename(oldName, family); } /** * Return an Icon Family * @param name * @return */ public static IconFamily getIconFamily(String name) { return getInstance().getFamily(name); } /** * Add an Icon Family. Don't overwrite an existing Icon Family * of the same name. * * @param family */ private void add(IconFamily family) { if (iconFamilies.get(family.getName()) == null) { iconFamilies.put(family.getName(), family); } } /** * Remove an Icon Family. Ensure that the family to be removed is * the same as the one on the list. * @param family */ private void remove(IconFamily family) { final IconFamily old = iconFamilies.get(family.getName()); if (old != null && old == family) { iconFamilies.remove(family.getName()); } } /** * Rename an IconFamily. Do not affect existing families with the * same name. * * @param oldFamilyName * @param iconFamily */ private void rename(String oldFamilyName, IconFamily iconFamily) { final IconFamily oldFamily = iconFamilies.get(oldFamilyName); if (oldFamily != null && oldFamily == iconFamily) { iconFamilies.remove(oldFamilyName); } add(iconFamily); } /** * Return an individual named IconFamily. * Ensure the Vassal icon prescan has completed first. * * @param iconFamilyName * @return Icon Family */ IconFamily getFamily(String iconFamilyName) { try { // FIXME: This is bad---we should wait on a Future instead. // Ensure preload is complete if (preloadThread.isAlive()) { try { preloadThread.join(); } catch (InterruptedException e) { } } return iconFamilies.get(iconFamilyName); } catch (IllegalStateException e) { ErrorDialog.bug(e); } return null; } /** * Return a sorted list of all available IconFamily names. * * @return Icon Family name list */ private List<String> getIconFamilyList() { final ArrayList<String> names = new ArrayList<String>(); synchronized (preloadLock) { for (String s : iconFamilies.keySet()) { names.add(s); } } Collections.sort(names); return names; } /** ------------------------------------------------------------------- * Inspect the Jar file (for a standard installation) or the local file * system (Vassal running under a debugger) and determine all available * Icons of all sizes and collect them into named Icon Familys. * * This is essentially a cross-reference of all available Icons to ensure * fast processing of requests for Icons. No Icons are created at this * stage. */ private void initVassalIconFamilys() { URL imageUrl = null; try { //Build a URL to the Vassal images folder. It is guaranteed to exist // in any version of Vassal imageUrl = jar.getURL(DataArchive.IMAGE_DIR); logger.info("VASSAL images folder found at "+imageUrl); // Determine if we are running locally under a debugger, or // from an installation package. If running an installed version // of Vassal, the images URL will start with "jar:". if (imageUrl.toString().startsWith(FILE)) { findLocalScalableIcons(); for (int size = 0; size < IconFamily.SIZE_DIRS.length; size++) { findLocalSizedIcons(size); } } else if (imageUrl.toString().startsWith(JAR)) { findJarIcons(); } else { throw new IllegalBuildException ("Unknown Vassal Image source type: "+imageUrl.toString()); //$NON-NLS-1$ } } catch (IOException e) { ReadErrorDialog.error(e, imageUrl.toString()); } } /** * Record all icons of the specified size found in the local file system * NB. Vassal is not running from a bundled Jar file * * @param size * @throws IOException */ protected void findLocalSizedIcons(int size) throws IOException { final String path = DataArchive.ICON_DIR+IconFamily.SIZE_DIRS[size]; final URL sizeURL = jar.getURL(path); BufferedReader br = null; try { br = new BufferedReader(new InputStreamReader(sizeURL.openStream())); String imageName = ""; //$NON-NLS-1$ while (imageName != null) { imageName = br.readLine(); if (imageName != null) { final String familyName = imageName.split("\\.")[0]; //$NON-NLS-1$ IconFamily family = iconFamilies.get(familyName); if (family == null) { family = new IconFamily(familyName); logger.info("Icon family "+familyName+" created for "+imageName); } family.setSizeIconPath(size, "/" + path + imageName); //$NON-NLS-1$ //$NON-NLS-2$ iconFamilies.put(familyName, family); } } } finally { IOUtils.closeQuietly(br); } } /** * Record all scalable icons found on the local file system. * NB. Vassal is not running from a bundled Jar file * * @throws IOException */ private void findLocalScalableIcons() throws IOException { final String scalablePath = DataArchive.ICON_DIR+IconFamily.SCALABLE_DIR; final URL url = jar.getURL(scalablePath); BufferedReader br = null; try { br= new BufferedReader(new InputStreamReader(url.openStream())); String imageName = ""; //$NON-NLS-1$ while (imageName != null) { imageName = br.readLine(); if (imageName != null) { final String familyName = imageName.split("\\.")[0]; //$NON-NLS-1$ IconFamily family = iconFamilies.get(familyName); if (family == null) { family = new IconFamily(familyName); logger.info("Icon family "+familyName+" created for "+imageName); } family.setScalableIconPath("/" + scalablePath + imageName); //$NON-NLS-1$ //$NON-NLS-2$ iconFamilies.put(familyName, family); } } } finally { IOUtils.closeQuietly(br); } } /** * Process the installed Vassal JarFile to find contained Icons * @throws IOException */ private void findJarIcons() throws IOException { // Path to scalable icons final String scalablePath = DataArchive.ICON_DIR+IconFamily.SCALABLE_DIR; // Path to sized icons final String[] sizePaths = new String[IconFamily.SIZE_COUNT]; for (int size = 0; size < IconFamily.SIZE_COUNT; size++) { sizePaths[size] = DataArchive.ICON_DIR+IconFamily.SIZE_DIRS[size]; } final JarURLConnection j = (JarURLConnection) jar.getURL(DataArchive.IMAGE_DIR).openConnection(); final JarFile vengine = j.getJarFile(); for (Enumeration<JarEntry> e = vengine.entries(); e.hasMoreElements();) { final JarEntry entry = e.nextElement(); final String entryName = entry.getName(); if (entryName.startsWith(DataArchive.ICON_DIR)) { if (entryName.startsWith(scalablePath) && ! entryName.equals(scalablePath)) { final String imageName = entryName.substring(scalablePath.length()); final String familyName = imageName.split("\\.")[0]; //$NON-NLS-1$ IconFamily family = iconFamilies.get(familyName); if (family == null) { family = new IconFamily(familyName); logger.info("Icon family "+familyName+" created for "+imageName); } family.setScalableIconPath("/" + entryName); //$NON-NLS-1$ iconFamilies.put(familyName, family); continue; } for (int size = 0; size < IconFamily.SIZE_COUNT; size++) { if (entryName.startsWith(sizePaths[size]) && ! entryName.equals(sizePaths[size])) { final String imageName = entryName.substring(sizePaths[size].length()); final String familyName = imageName.split("\\.")[0]; //$NON-NLS-1$ IconFamily family = iconFamilies.get(familyName); if (family == null) { family = new IconFamily(familyName); logger.info("Icon family "+familyName+" created for "+imageName); } family.setSizeIconPath(size, "/" + entryName); //$NON-NLS-1$ iconFamilies.put(familyName, family); break; } } } } } }