gt:       while ((zipEntry=input.getNextEntry()) != null) {
context: /*
   * Open Source Physics software is free software as described near the bottom of
   * this code file.
   *
   * For additional information and documentation on Open Source Physics please
   * see: <http://www.opensourcephysics.org/>
   */
  
  package org.opensourcephysics.tools;
  import java.applet.AudioClip;
  import java.awt.Component;
  import java.awt.Image;
  import java.awt.Toolkit;
  import java.awt.image.BufferedImage;
  import java.io.BufferedInputStream;
  import java.io.File;
  import java.io.FileFilter;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.FileWriter;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.io.Reader;
  import java.net.HttpURLConnection;
  import java.net.MalformedURLException;
  import java.net.URL;
  import java.net.URLClassLoader;
  import java.util.ArrayList;
  import java.util.Enumeration;
  import java.util.HashSet;
  import java.util.Hashtable;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.TreeMap;
  import java.util.TreeSet;
  import java.util.zip.ZipEntry;
  import java.util.zip.ZipFile;
  import java.util.zip.ZipInputStream;
  
  import javax.swing.ImageIcon;
  import javax.swing.JFileChooser;
  import javax.swing.JOptionPane;
  
  import org.opensourcephysics.controls.OSPLog;
  import org.opensourcephysics.controls.XML;
  import org.opensourcephysics.display.OSPRuntime;
  
  /**
   * This defines static methods for loading resources.
   *
   * @author Douglas Brown
   * @version 1.0
   */
  public class ResourceLoader {
  	
    @SuppressWarnings("javadoc")
  	public static final FileFilter OSP_CACHE_FILTER;
    protected static final String WIN_XP_DEFAULT_CACHE = "/Local Settings/Application Data/OSP/Cache"; //$NON-NLS-1$
    protected static final String WINDOWS_DEFAULT_CACHE = "/AppData/Local/OSP/Cache"; //$NON-NLS-1$
    protected static final String OSX_DEFAULT_CACHE = "/Library/Caches/OSP"; //$NON-NLS-1$
    protected static final String LINUX_DEFAULT_CACHE = "/Downloads/OSP/Cache"; //$NON-NLS-1$
    protected static final String SEARCH_CACHE_SUBDIRECTORY = "Search"; //$NON-NLS-1$
  
    protected static ArrayList<String> searchPaths = new ArrayList<String>();                        // search paths
    protected static ArrayList<String> appletSearchPaths = new ArrayList<String>();                  // search paths for apples
    protected static int maxPaths = 20;                                                              // max number of paths in history
    protected static Hashtable<String, Resource> resources = new Hashtable<String, Resource>();      // cached resources
    protected static boolean cacheEnabled=false, canceled=false;
    protected static Map<String, URLClassLoader> zipLoaders = new TreeMap<String, URLClassLoader>(); // maps path to zipLoader
    protected static URLClassLoader xsetZipLoader; // zipLoader of current xset
    protected static Set<String> extractExtensions = new TreeSet<String>();
    protected static ArrayList<String> pathsNotFound = new ArrayList<String>();
    protected static File ospCache;
    protected static boolean zipURLsOK;
    
    static {
    	OSP_CACHE_FILTER = new FileFilter() {
    		public boolean accept(File file) {
    			return file.isDirectory() && file.getName().startsWith("osp-"); //$NON-NLS-1$
    		}
    	};
    }
  
    /**
     * Private constructor to prevent instantiation.
     */
    private ResourceLoader() {
      /** empty block */
    }
  
    /**
     * Gets a resource specified by name. If no resource is found using the name
     * alone, the searchPaths are searched.
     *
     * @param name the file or URL name
     * @return the Resource, or null if none found
     */
    public static Resource getResource(String name) {
      return getResource(name, true);
    }
  
    /**
     * Gets a resource specified by name. If no resource is found using the name
     * alone, the searchPaths are searched. This will find a zip file as a URL
     * resource, unlike the getResource(String) method.
     *
     * @param name the file or URL name
     * @return the Resource, or null if none found
     */
    public static Resource getResourceZipURLsOK(String name) {
    	zipURLsOK = true;
      Resource res = getResource(name, true);
    	zipURLsOK = false;
      return res;
    }
  
    /**
     * Gets a resource specified by name. If no resource is found using
     * the name alone, the searchPaths are searched.
     * Files are searched only if searchFile is true.
     *
     * @param name the file or URL name
     * @param searchFiles true to search files
     * @return the Resource, or null if none found
     */
    public static Resource getResource(String name, boolean searchFiles) {
      try {
        URL url = getAppletResourceURL(name); // added by W. Christian
        if(url!=null) {
          return new Resource(url);
        }
      } catch(Exception ex) {}
      return getResource(name, Resource.class, searchFiles);
    }
  
    /**
     * Gets a resource specified by name and Class. If no resource is found using
     * the name alone, the searchPaths are searched.
     *
     * @param name the file or URL name
     * @param type the Class providing default ClassLoader resource loading
     * @return the Resource, or null if none found
     */
    public static Resource getResource(String name, Class<?> type) {
      return getResource(name, type, true);
    }
  
    /**
     * Gets a resource specified by name and Class. If no resource is found using
     * the name alone, the searchPaths are searched.
     * Files are searched only if searchFile is true.
     *
     * @param name the file or URL name
     * @param type the Class providing default ClassLoader resource loading
     * @param searchFiles true to search files
     * @return the Resource, or null if none found
     */
    public static Resource getResource(String name, Class<?> type, boolean searchFiles) {
      if((name==null)||name.equals("")) { //$NON-NLS-1$
        return null;
      }
      pathsNotFound.clear();
      // Remove leading and trailing inverted commas (added by Paco)
      if(name.startsWith("\"")) { //$NON-NLS-1$   
        name = name.substring(1);
      }
      if(name.endsWith("\"")) { //$NON-NLS-1$   
        name = name.substring(0, name.length()-1);
      }
      if(name.startsWith("./")) { //$NON-NLS-1$   
        name = name.substring(2);
      }
      if(OSPRuntime.isAppletMode()||(OSPRuntime.applet!=null)) { // added by Paco
        Resource appletRes = null;
        // following code added by Doug Brown 2009/11/14
        if(type==OSPRuntime.applet.getClass()) {
          try {
            URL url = type.getResource(name);
            appletRes = createResource(url);
            if(appletRes!=null) {
              return appletRes;
            }
          } catch(Exception ex) {}
        }  // end code added by Doug Brown 2009/11/14
        for(Iterator<String> it = searchPaths.iterator(); it.hasNext(); ) {
          String path = getPath(it.next(), name);
          appletRes = findResourceInClass(path, type, searchFiles);
          if(appletRes!=null) {
            return appletRes;
          }
        }
        appletRes = findResourceInClass(name, type, searchFiles);
        if(appletRes!=null) {
          return appletRes;
        }
      }
      // look for resource with name only
      Resource res = findResource(name, type, searchFiles);
      if(res!=null) {
        return res;
      }
      pathsNotFound.add(name);
      StringBuffer err = new StringBuffer("Not found: "+name); //$NON-NLS-1$
      err.append(" [searched "+name); //$NON-NLS-1$
      // look for resource in searchPaths
      for(String next: searchPaths) {
        String path = getPath(next, name);
      	if (pathsNotFound.contains(path))
      		continue;
        res = findResource(path, type, searchFiles);
        if(res!=null) {
          return res;
        }
        pathsNotFound.add(path);
        err.append(";"+path); //$NON-NLS-1$
      }
      err.append("]"); //$NON-NLS-1$
      OSPLog.fine(err.toString());
      return null;
    }
  
    /**
     * Gets a resource specified by base path and name. If base path is relative
     * and no resource is found using the base alone, the searchPaths are
     * searched.
     *
     * @param basePath the base path
     * @param name the file or URL name
     * @return the Resource, or null if none found
     */
    public static Resource getResource(String basePath, String name) {
      return getResource(basePath, name, Resource.class);
    }
  
    /**
     * Gets a resource specified by base path and name. If base path is relative
     * and no resource is found using the base alone, the searchPaths are
     * searched. Files are searched only if searchFile is true.
     *
     * @param basePath the base path
     * @param name the file or URL name
     * @param searchFiles true to search files
     * @return the Resource, or null if none found
     */
    public static Resource getResource(String basePath, String name, boolean searchFiles) {
      return getResource(basePath, name, Resource.class, searchFiles);
    }
  
    /**
     * Gets a resource specified by base path, name and class. If base path is
     * relative and no resource is found using the base alone, the searchPaths
     * are searched.
     *
     * @param basePath the base path
     * @param name the file or URL name
     * @param type the Class providing ClassLoader resource loading
     * @return the Resource, or null if none found
     */
    public static Resource getResource(String basePath, String name, Class<Resource> type) {
      return getResource(basePath, name, type, true);
    }
  
    /**
   * Gets a resource specified by base path, name and class. If base path is
   * relative and no resource is found using the base alone, the searchPaths
   * are searched. Files are searched only if searchFile is true.
   *
   * @param basePath the base path
   * @param name the file or URL name
   * @param type the Class providing ClassLoader resource loading
   * @param searchFiles true to search files
   * @return the Resource, or null if none found
   */
    public static Resource getResource(String basePath, String name, Class<Resource> type, boolean searchFiles) {
    	if(basePath==null) {
        return getResource(name, type);
      }
      if(name.startsWith("./")) { //$NON-NLS-1$   
        name = name.substring(2);
      }
      // look for resource with basePath and name
      pathsNotFound.clear();
      String path = getPath(basePath, name);
      Resource res = findResource(path, type, searchFiles);
      if(res!=null) {
        return res;
      }
      // keep looking only if base path is relative
      if(basePath.startsWith("/")||(basePath.indexOf(":/")>-1)) { //$NON-NLS-1$ //$NON-NLS-2$
        return null;
      }
      pathsNotFound.add(path);
      StringBuffer err = new StringBuffer("Not found: "+path); //$NON-NLS-1$
      err.append(" [searched "+path); //$NON-NLS-1$
      if(OSPRuntime.applet!=null) {                  // applet mode
        String docBase = OSPRuntime.applet.getDocumentBase().toExternalForm();
        docBase = XML.getDirectoryPath(docBase)+"/"; //$NON-NLS-1$
        path = getPath(getPath(docBase, basePath), name);
      	if (!pathsNotFound.contains(path)) {
  	      res = findResource(path, type, searchFiles);
  	      if(res!=null) {
  	        return res;
  	      }
  	      pathsNotFound.add(path);
  	      err.append(";"+path);                        //$NON-NLS-1$
      	}
        String codeBase = OSPRuntime.applet.getCodeBase().toExternalForm();
        if(!codeBase.equals(docBase)) {
          path = getPath(getPath(codeBase, basePath), name);
        	if (!pathsNotFound.contains(path)) {
  	        res = findResource(path, type, searchFiles);
  	        if(res!=null) {
  	          return res;
  	        }
  	        pathsNotFound.add(path);
  	        err.append(";"+path);                      //$NON-NLS-1$
        	}
        }
      }
      // look for resource in searchPaths
      for(Iterator<String> it = searchPaths.iterator(); it.hasNext(); ) {
        path = getPath(getPath(it.next(), basePath), name);
      	if (pathsNotFound.contains(path))
      		continue;
        res = findResource(path, type, searchFiles);
        if(res!=null) {
          return res;
        }
        pathsNotFound.add(path);
        err.append(";"+path); //$NON-NLS-1$
      }
      err.append("]"); //$NON-NLS-1$
      OSPLog.fine(err.toString());
      return null;
    }
  
    /**
     * Adds a path at the beginning of the searchPaths list.
     *
     * @param base the base path to add
     */
    public static void addSearchPath(String base) {
      if((base==null)||base.equals("")||(maxPaths<1)) { //$NON-NLS-1$
        return;
      }
      synchronized(searchPaths) {
        if(searchPaths.contains(base)) {
          searchPaths.remove(base);
        } else {
          OSPLog.fine("Added path: "+base);   //$NON-NLS-1$
        }
        searchPaths.add(0, base);
        while(searchPaths.size()>Math.max(maxPaths, 0)) {
          base = searchPaths.get(searchPaths.size()-1);
          OSPLog.fine("Removed path: "+base); //$NON-NLS-1$
          searchPaths.remove(base);
        }
      }
    }
  
    /**
     * Removes a path from the searchPaths list.
     *
     * @param base the base path to remove
     */
    public static void removeSearchPath(String base) {
      if((base==null)||base.equals("")) { //$NON-NLS-1$
        return;
      }
      synchronized(searchPaths) {
        if(searchPaths.contains(base)) {
          OSPLog.fine("Removed path: "+base); //$NON-NLS-1$
          searchPaths.remove(base);
        }
      }
    }
  
    /**
     * Adds a search path at the beginning of the applet's search path list.
     * Added by Wolfgang Christian.
     *
     * @param base the base path to add
     */
    public static void addAppletSearchPath(String base) {
      if((base==null)||(maxPaths<1)) {
        return;
      }
  	base=base.trim();
  	if(!base.endsWith("/"))base=base+"/";  //$NON-NLS-1$//$NON-NLS-2$
      synchronized(appletSearchPaths) {
        if(appletSearchPaths.contains(base)) {
          appletSearchPaths.remove(base);                 // search path will be added to top of list later
        } else {
          OSPLog.fine("Applet search path added: "+base); //$NON-NLS-1$
        }
        appletSearchPaths.add(0, base);
        while(appletSearchPaths.size()>Math.max(maxPaths, 0)) {
          base = appletSearchPaths.get(appletSearchPaths.size()-1);
          OSPLog.fine("Removed path: "+base);             //$NON-NLS-1$
          appletSearchPaths.remove(base);
        }
      }
    }
  
    /**
     * Removes a path from the applet search path list.
     * Added by Wolfgang Christian.
     *
     * @param base the base path to remove
     */
    public static void removeAppletSearchPath(String base) {
      if((base==null)||base.equals("")) { //$NON-NLS-1$
        return;
      }
      synchronized(appletSearchPaths) {
        if(appletSearchPaths.contains(base)) {
          OSPLog.fine("Applet search path removed: "+base); //$NON-NLS-1$
          appletSearchPaths.remove(base);
        }
      }
    }
  
    /**
     * Sets the cacheEnabled property.
     *
     * @param enabled true to enable the cache
     */
    public static void setCacheEnabled(boolean enabled) {
      cacheEnabled = enabled;
    }
  
    /**
     * Gets the cacheEnabled property.
     *
     * @return true if the cache is enabled
     */
    public static boolean isCacheEnabled() {
      return cacheEnabled;
    }
  
    /**
     * Adds an extension to the end of the extractExtensions list.
     * Files with this extension found inside jars are extracted before loading.
     *
     * @param extension the extension to add
     */
    public static void addExtractExtension(String extension) {
      if((extension==null)||extension.equals("")) { //$NON-NLS-1$
        return;
      }
      if(!extension.startsWith(".")) { //$NON-NLS-1$
        extension = "."+extension;     //$NON-NLS-1$
      }
      OSPLog.finest("Added extension: "+extension); //$NON-NLS-1$
      synchronized(extractExtensions) {
        extractExtensions.add(extension);
      }
    }
  
    /**
     * Cancels the current operation when true.
     *
     * @param cancel true to cancel
     */
    public static void setCanceled(boolean cancel) {
    	canceled = cancel;
    }
    
    /**
     * Determines if the current operation is canceled.
     *
     * @return true if canceled
     */
    public static boolean isCanceled() {
    	return canceled;
    }
    
    // ___________________________ convenience methods _________________________
    
    /**
     * Opens and returns an input stream. May return null.
     * 
     * @param path the path
     * @return the input stream
     */
    public static InputStream openInputStream(String path) {
      Resource res = getResource(path);
      return(res==null) ? null : res.openInputStream();
    }
  
    /**
     * Opens and returns a reader. May return null.
     * 
     * @param path the path
     * @return the reader
     */
    public static Reader openReader(String path) {
      Resource res = getResource(path);
      return(res==null) ? null : res.openReader();
    }
  
    /**
     * Gets a string. May return null.
     * 
     * @param path the path
     * @return the string
     */
    public static String getString(String path) {
      Resource res = getResource(path);
      return(res==null) ? null : res.getString();
    }
  
    /**
     * Gets an image icon. May return null.
     * 
     * @param path the path
     * @return the icon
     */
    public static ImageIcon getIcon(String path) {
      URL url = getAppletResourceURL(path); // added by W. Christian
      if(url!=null) {
        return new ImageIcon(url);
      }
      Resource res = getResource(path);
      return(res==null) ? null : res.getIcon();
    }
  
    /**
     * Gets an image. May return null.
     * 
     * @param path the path
     * @return the image
     */
    public static Image getImage(String path) {
      URL url = getAppletResourceURL(path); // added by W. Christian
      if(url!=null) {
        return new ImageIcon(url).getImage();
      }
      Resource res = getResource(path);
      return(res==null) ? null : res.getImage();
    }
  
    /**
     * Gets a buffered image. May return null.
     * 
     * @param path the path
     * @return the image
     */
    public static BufferedImage getBufferedImage(String path) {
      Resource res = getResource(path);
      return(res==null) ? null : res.getBufferedImage();
    }
  
    /**
    * Gets a buffered image. May return null.
    * 
    * @param path the path
     * @param bufferedImageType one of the types defined by the BufferedImage class
    * @return the image
    */
   public static BufferedImage getBufferedImage(String path, int bufferedImageType) {
     Resource res = getResource(path);
     return(res==null) ? null : res.getBufferedImage(bufferedImageType);
   }
  
    /**
     * Gets an audio clip. May return null.
     * 
     * @param path the path
     * @return the audio clip
     */
    public static AudioClip getAudioClip(String path) {
      Resource res = getResource(path);
      return(res==null) ? null : res.getAudioClip();
    }
    
    /**
     * Sets the directory for cached files.
     * 
     * @param newCache the desired cache directory
     */
    public static void setOSPCache(File newCache) {
    	if (newCache!=null && !newCache.equals(ospCache)) {
    		// reject new cache if it is a subdirectory of the current cache!
    		if (ospCache!=null && newCache.getAbsolutePath().contains(ospCache.getAbsolutePath())) {
          Toolkit.getDefaultToolkit().beep();
    			return;
    		}
        if (!newCache.exists() || !newCache.isDirectory()) {
        	if (!newCache.mkdirs()) {
            Toolkit.getDefaultToolkit().beep();
            return;
        	}
        }
        if (!newCache.canWrite()) {
          Toolkit.getDefaultToolkit().beep();
          return;
        }
        if (ospCache!=null) {
  	      // copy existing cached files into new cache and delete old cache
  		  	File[] hostDirectories = ospCache.listFiles(OSP_CACHE_FILTER);
        	for (File host: hostDirectories) {
        		String hostname = host.getName();
        		File newHost = new File(newCache, hostname);
        		copyAllFiles(host, newHost);
        	}
        	
        	// copy search cache files into new cache
        	File searchCache = new File(ospCache, SEARCH_CACHE_SUBDIRECTORY);
        	File newSearchCache = new File(newCache, SEARCH_CACHE_SUBDIRECTORY);
      		copyAllFiles(searchCache, newSearchCache);
      		
        	// clear prev cache, including search cache
      		clearOSPCache(ospCache, true); 
      		// delete prev cache only if it is empty
  		  	File[] files = ospCache.listFiles();
  		  	if (files!=null && files.length==0) {
  		  		ospCache.delete();
  				}
        }
      	ospCache = newCache;
    	}
    }
  
    /**
     * Gets the directory for cached files.
     * 
     * @return the OSP cache
     */
    public static File getOSPCache() {
    	return ospCache;
    }
    
    /**
     * Gets the default directory for cached files.
     * 
     * @return the default OSP cache
     */
    public static File getDefaultOSPCache() {
    	String cachePath = System.getProperty("java.io.tmpdir"); //$NON-NLS-1$
    	String userHome = System.getProperty("user.home"); //$NON-NLS-1$
  		if (userHome!=null) {
  	  	userHome += "/"; //$NON-NLS-1$
  			if (OSPRuntime.isMac()) {
  				cachePath = userHome+ResourceLoader.OSX_DEFAULT_CACHE;
  			}
  			else if (OSPRuntime.isLinux()) {
  				cachePath = userHome+ResourceLoader.LINUX_DEFAULT_CACHE;
  			}
  			else if (OSPRuntime.isWindows()) {
  				String os = System.getProperty("os.name", "").toLowerCase(); //$NON-NLS-1$ //$NON-NLS-2$
  				if (os.indexOf("xp")>-1) //$NON-NLS-1$
  					cachePath = userHome+ResourceLoader.WIN_XP_DEFAULT_CACHE;
  				else
  					cachePath = userHome+ResourceLoader.WINDOWS_DEFAULT_CACHE;
  			}
  		}
  		if (cachePath==null) return null;
  		return new File(cachePath);
    }
    
    /**
     * Uses a JFileChooser to select a cache directory.
     * @param parent  a component to own the file chooser
     * @return the chosen file
     */
    public static File chooseOSPCache(Component parent) {
      JFileChooser chooser = new JFileChooser(getOSPCache());
      if (OSPRuntime.isMac())
        chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
      else
      	chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
      javax.swing.filechooser.FileFilter folderFilter = new javax.swing.filechooser.FileFilter() {
        // accept directories only
        public boolean accept(File f) {
        	if (f==null) return false;
          return f.isDirectory();
        }
        public String getDescription() {
          return ToolsRes.getString("LibraryTreePanel.FolderFileFilter.Description"); //$NON-NLS-1$
        } 	     	
      };
      chooser.setAcceptAllFileFilterUsed(false);
      chooser.addChoosableFileFilter(folderFilter);
      String text = ToolsRes.getString("ResourceLoader.FileChooser.Cache"); //$NON-NLS-1$
      chooser.setDialogTitle(text);
  	  int result = chooser.showDialog(parent, text);
      if (result==JFileChooser.APPROVE_OPTION) {
        return chooser.getSelectedFile();
      }
    	return null;
    }
    
  
    
    /**
     * Determines if a path defines a file in the OSP cache.
     * 
     * @param path the path
     * @return true if path is in the OSP cache
     */
    public static boolean isOSPCachePath(String path) {
    	File cacheFile = getOSPCache();
    	if (cacheFile==null)
  			cacheFile = new File(System.getProperty("java.io.tmpdir")); //$NON-NLS-1$	
  		String cachePath = XML.forwardSlash(cacheFile.getAbsolutePath())+"/osp-"; //$NON-NLS-1$
    	if (XML.forwardSlash(path).contains(cachePath)) return true;
    	return false;
    }
    
    /**
     * Gets the cache file associated with a URL path.
     * 
     * @param urlPath the path to the file
     * @return the cache file
     */
    public static File getOSPCacheFile(String urlPath) {
    	return getOSPCacheFile(urlPath, null);
    }
  	
  	/**
     * Gets the cache file associated with a URL path.
     * 
     * @param urlPath the path to the file
     * @param name name of the file (may be null)
     * @return the cache file
     */
    public static File getOSPCacheFile(String urlPath, String name) {
    	File cacheFile = getOSPCache();
    	if (cacheFile==null)
  			cacheFile = new File(System.getProperty("java.io.tmpdir")); //$NON-NLS-1$	
    	return getCacheFile(cacheFile, urlPath, name);
    }
  
    /**
     * Gets the search cache directory.
     * 
     * @return the search cache
     */
    public static File getSearchCache() {
    	File ospCache = getOSPCache();
    	if (ospCache==null) {
    		ospCache = getDefaultOSPCache();
    	}
    	if (ospCache==null) return null;
    	File searchCache = new File(ospCache, SEARCH_CACHE_SUBDIRECTORY);
    	searchCache.mkdirs();
    	return searchCache;
    }
    
  	/**
     * Gets the search cache (XML) file associated with a URL path.
     * 
     * @param urlPath the path to the file
     * @return the search cache file
     */
    public static File getSearchCacheFile(String urlPath) {
  		String filename = XML.getName(urlPath);
    	String basename = XML.stripExtension(filename);
    	String ext = XML.getExtension(filename);
    	if (ext!=null) 
    		basename += "_"+ext; //$NON-NLS-1$
    	// following line needed to clean up long extensions associated with ComPADRE query paths
    	basename = basename.replace('&', '_').replace('?', '_').replace('=', '_');
    	filename = basename+".xml"; //$NON-NLS-1$
    	return getCacheFile(getSearchCache(), urlPath, filename);
    }
  
  	/**
     * Gets the cache file associated with a base cache directory and URL path.
     * @param cacheFile the base cache directory
     * @param urlPath the URL path to the original file
     * @param name name of the file (may be null)
     * @return the cache file
     */
    private static File getCacheFile(File cacheFile, String urlPath, String name) {
  		String cachePath = XML.forwardSlash(cacheFile.getAbsolutePath());
  		urlPath = getURIPath(urlPath);
    		
  		String host = ""; //$NON-NLS-1$
  		String path = ""; //$NON-NLS-1$
  		String filename = ""; //$NON-NLS-1$
  		try {
  			URL url = new URL(urlPath);
  			host = url.getHost().replace('.', '_');
  			path = getNonURIPath(url.getPath());
  			
  			int n = path.indexOf(cachePath);
  			if (n>-1) {
  				path = path.substring(n+cachePath.length());
  			}
  			// if path is local, strip drive letter
  			n = path.lastIndexOf(":"); //$NON-NLS-1$
  			if (n>-1) {
  				path = path.substring(n+1);
  			}
  			// strip leading slash
    		while (path.startsWith("/")) { //$NON-NLS-1$
    			path = path.substring(1);
    		}
  			String pathname = XML.getName(path);
  			if (!"".equals(pathname)) { //$NON-NLS-1$
  				path = XML.getDirectoryPath(path);
  			}
  			path = path.replace('.', '_').replace("!", ""); //$NON-NLS-1$ //$NON-NLS-2$
  			filename = name==null? pathname: name;
  		} catch (MalformedURLException e) {				
  		}
  		
  		if ("".equals(host)) //$NON-NLS-1$
  			host = "local_machine"; //$NON-NLS-1$		
  		if (!path.startsWith("osp-"))  //$NON-NLS-1$
  			cacheFile = new File(cacheFile, "osp-"+host); //$NON-NLS-1$
  		if (!"".equals(path)) //$NON-NLS-1$
  			cacheFile = new File(cacheFile, path);
  		if (!"".equals(filename)) //$NON-NLS-1$
  			cacheFile = new File(cacheFile, filename);
  		
  		return cacheFile;
    }
  
    /**
     * Downloads a file from the web to the OSP Cache.
     * 
     * @param urlPath the path to the file
     * @param fileName the name to assign the downloaded file
     * @param alwaysOverwrite true to overwrite an existing file, if any
     * @return the downloaded file, or null if failed to download
     */
    public static File downloadToOSPCache(String urlPath, String fileName, boolean alwaysOverwrite) {
  		if (fileName==null) return null;
    	File target = getOSPCacheFile(urlPath, fileName);
  		File file = ResourceLoader.download(urlPath, target, alwaysOverwrite);
  		if (file==null) {
  			JOptionPane.showMessageDialog(null, 
  					ToolsRes.getString("ResourceLoader.Dialog.FailedToDownload.Message1") //$NON-NLS-1$ 
  					+"\n"+ToolsRes.getString("ResourceLoader.Dialog.FailedToDownload.Message2") //$NON-NLS-1$ //$NON-NLS-2$ 
  					+"\n"+ToolsRes.getString("ResourceLoader.Dialog.FailedToDownload.Message3"), //$NON-NLS-1$ //$NON-NLS-2$ 
  					ToolsRes.getString("ResourceLoader.Dialog.FailedToDownload.Title"), //$NON-NLS-1$ 
  					JOptionPane.ERROR_MESSAGE);
  		}
  		return file;
    }
    
    /**
     * Returns the HTML code for a local or web HTML page.
     * @param path the path to the HTML page 
     * @return the HTML code, or null if not found or not HTML
     */
  	public static String getHTMLCode(String path) {
  		Resource res = getResourceZipURLsOK(path);
  		if (res==null) return null;
  		String html = res.getString();
  		if (html!=null && html.trim().startsWith("<!DOCTYPE html")) //$NON-NLS-1$
  			return html;
  		return null;
  	}
  	
    /**
     * Returns the title, if any, of an HTML page.
     * @param code the HTML code 
     * @return the title, or null if none defined
     */
  	public static String getTitleFromHTMLCode(String code) {
  		if (code==null) return null;
  		String[] parts = code.split("<title>"); //$NON-NLS-1$
  		if (parts.length>1) {
  			parts = parts[1].split("</title>"); //$NON-NLS-1$
  			if (parts.length>1) {
  				return parts[0].trim();
  			}
  		}
  		return null;
  	}
  	
    /**
     * Returns the first stylesheet link, if any, in an HTML page.
     * @param code the HTML code 
     * @return the first stylesheet link found, or null if none
     */
  	public static String getStyleSheetFromHTMLCode(String code) {
  		if (code==null) return null;
    	// typical tag is in head: <link rel="stylesheet" type="text/css" href="myFolder/myStyleSheet.css">
  		String[] parts = code.split("<head>"); //$NON-NLS-1$
  		if (parts.length>1) {
  			parts = parts[1].split("</head>"); //$NON-NLS-1$
  			if (parts.length>1) {
  				parts = parts[0].split("<link"); //$NON-NLS-1$
  				if (parts.length>1) {
  					// skip parts[0]
  					for (int i=1; i<parts.length; i++) {
  			  		if (parts[i].contains("\"stylesheet\"")) { //$NON-NLS-1$
  			  			parts = parts[i].split("href"); //$NON-NLS-1$
  							if (parts.length>1) {
  								parts = parts[1].split("\""); //$NON-NLS-1$
  								if (parts.length>1) return parts[1];
  							}
  			  		}
  					}
  				}
  
  			}
  		}  	
  		return null;
  	}
  	
    /**
     * Copies an HTML file with associated images and stylesheet to the OSP cache.
     * Note this does NOT overwrite cache files--to replace, delete them before calling this method
     * @param htmlPath the path to the source HTML file
     * @return the copied File, or null if failed
     */
    public static File copyHTMLToOSPCache(String htmlPath) {
    	if (htmlPath==null) return null;
    	// read html text
    	String htmlCode = null;
    	Resource res = getResourceZipURLsOK(htmlPath);
    	if (res!=null) {
    		// if the resource is a file in the OSP cache, return it 
  	  	if (res.getFile()!=null && isOSPCachePath(htmlPath)) {
  	  		return res.getFile();
  	  	}
    		htmlCode = res.getString();
    	}
    	if (htmlCode!=null) {
    		String htmlBasePath = XML.getDirectoryPath(htmlPath);
    		File htmlTarget = getOSPCacheFile(htmlPath);
    		File targetDirectory = htmlTarget.getParentFile();
    		targetDirectory.mkdirs();
    		
  	  	// look for image references
  	  	File imageDir = new File(targetDirectory, "images"); //$NON-NLS-1$
    		String img = "<img "; //$NON-NLS-1$
    		String pre = "src=\""; //$NON-NLS-1$
    		String post = "\""; //$NON-NLS-1$
    		
    		String temp = htmlCode;
    		int j = temp.indexOf(img);
  	  	if (j>-1) imageDir.mkdirs();
    		while (j>-1) {
    			temp = temp.substring(j+img.length());
    			j = temp.indexOf(pre);
    			temp = temp.substring(j+pre.length());
    			j = temp.indexOf(post);
    			if (j>-1) {
    				String next = temp.substring(0, j); // the image path specified in the html itself
    				String path = XML.getResolvedPath(next, htmlBasePath);
    	      res = getResourceZipURLsOK(path);
    				if (res!=null) {
        			// copy image and replace image path in html code
    					String filename = XML.getName(next);
        			File imageTarget = download(path, new File(imageDir, filename), false);
        			if (imageTarget!=null) {
  	      			path = XML.getPathRelativeTo(imageTarget.getAbsolutePath(), targetDirectory.getAbsolutePath());
  	      			if (!next.equals(path)) {
  	      				htmlCode = htmlCode.replace(pre+next+post, pre+path+post);
  	      			}      				
        			}
    				}
    			}
    			j = temp.indexOf(img);				
    		}
  	  	
  	  	// if separate stylesheet is used, copy to cache and replace in HTML code
  	  	String css = getStyleSheetFromHTMLCode(htmlCode);
  	  	if (css!=null && !css.startsWith("http:")) { //$NON-NLS-1$
  	  		res = getResourceZipURLsOK(XML.getResolvedPath(css, htmlBasePath));
  	  		if (res!=null) {
  	  			String cssName = XML.getName(css);
  	  			File cssTarget = new File(targetDirectory, cssName);
      			cssTarget = download(res.getAbsolutePath(), cssTarget, false);
      			if (cssTarget!=null && !cssName.equals(css)) {
        			htmlCode = htmlCode.replace(css, cssName);      				
      			}
  	  		}
  	  	}
    		
  	  	// write modified html text into target file
        try {
  	      FileWriter fout = new FileWriter(htmlTarget);
  	      fout.write(htmlCode);
  	      fout.close();
  		  	return htmlTarget;
  	    } catch(Exception ex) {
  	      ex.printStackTrace();
  	    }
    	}
    	return null;
    }
    
    /**
     * Copies a source file to a target file.
     * If source file is a directory, copies contents of directory, including subdirectories
     *
     * @param inFile the source
     * @param outFile the target
     * @return true if all files successfully copied
     */
    public static boolean copyAllFiles(File inFile, File outFile) {
    	if (inFile.isDirectory()) {
    		outFile.mkdir();
    		boolean success = true;
    		for (File in: inFile.listFiles()) {
    			String filename = in.getName();
    			File out = new File(outFile, filename);
    			success = success && copyAllFiles(in, out);
    		}
    		return success;
    	}
    	byte[] buffer = new byte[16384]; // 2^14
      try {
      	InputStream in = new FileInputStream(inFile);
      	OutputStream out = new FileOutputStream(outFile);
  			while (true) {
  				synchronized (buffer) {
  					int amountRead = in.read(buffer);
  					if (amountRead == -1) {
  						break;
  					}
  					out.write(buffer, 0, amountRead);
  				}
  			}
  			in.close();
  			out.close();
  		}                   
      catch (IOException ex) {
      	return false;
      }
    	return true;
    }
  
  
    
    /**
     * Clears an OSP cache. Always deletes "osp-host" directories in cache. Deletes search cache if requested.
     * 
     * @param cache the cache to clear
     * @param clearSearchCache true to clear the search cache
     * @return true if successfully cleared
     */
    public static boolean clearOSPCache(File cache, boolean clearSearchCache) {
    	if (cache==null || !cache.canWrite()) return false;
    	boolean success = true;
    	File[] files = cache.listFiles(OSP_CACHE_FILTER);
    	if (files==null) return true;
      for(File next: files) {
      	success = success && deleteFile(next);
      }
      if (clearSearchCache) {
      	File searchCache = new File(cache, SEARCH_CACHE_SUBDIRECTORY);
      	success = success && deleteFile(searchCache);    	
      }
      if (!success) {
  			JOptionPane.showMessageDialog(null, 
  					ToolsRes.getString("ResourceLoader.Dialog.UnableToClearCache.Message1") //$NON-NLS-1$ 
  					+"\n"+ToolsRes.getString("ResourceLoader.Dialog.UnableToClearCache.Message2"), //$NON-NLS-1$ //$NON-NLS-2$ 
  					ToolsRes.getString("ResourceLoader.Dialog.UnableToClearCache.Title"), //$NON-NLS-1$ 
  					JOptionPane.WARNING_MESSAGE);
      }
      return success;
    }
  
    /**
     * Clears an OSP cache host directory.
     * 
     * @param hostDir the cache host directory to clear
     * @return true if successfully cleared
     */
    public static boolean clearOSPCacheHost(File hostDir) {
    	if (hostDir==null || !hostDir.canWrite()) return true;
    	if (!OSP_CACHE_FILTER.accept(hostDir)) return false;
    	boolean success = deleteFile(hostDir);
      if (!success) {
  			JOptionPane.showMessageDialog(null, 
  					ToolsRes.getString("ResourceLoader.Dialog.UnableToClearCache.Message1") //$NON-NLS-1$ 
  					+"\n"+ToolsRes.getString("ResourceLoader.Dialog.UnableToClearCache.Message2"), //$NON-NLS-1$ //$NON-NLS-2$ 
  					ToolsRes.getString("ResourceLoader.Dialog.UnableToClearCache.Title"), //$NON-NLS-1$ 
  					JOptionPane.WARNING_MESSAGE);
      }
      return success;
    }
  
    /**
     * Deletes a file or folder. In case of a folder, deletes all contents
     * and the folder itself.
     * 
     * @param file the file to delete
     * @return true if deleted
     */
    public static boolean deleteFile(File file) {
      if(file.isDirectory()) {
        File[] files = file.listFiles();
        for(File next: files) {
          deleteFile(next);
        }
      }
      return file.delete();
    }
    
    /**
     * Gets the list of files in a directory and its subdirectories that are accepted by a FileFilter.
     * 
     * @param directory the directory to search
     * @param filter the FileFilter
     * @return the list of files
     */
    public static List<File> getFiles(File directory, FileFilter filter) {
      List<File> results = new ArrayList<File>();
      if (directory.isFile()) {
      	results.add(directory);
      	return results;
      }
      File[] contents = directory.listFiles(filter);
      for (File file: contents) {
        if (file.isDirectory()) {
          List<File> deeperList = getFiles(file, filter);
          results.addAll(deeperList);
        }
        else {
          results.add(file);       	
        }
      }
      return results;
    }
    
    /**
     * Gets the contents of a zip file.
     * 
     * @param zipPath the path to the zip file
     * @return a set of file names in alphabetical order
     */
    public static Set<String> getZipContents(String zipPath) {
      Set<String> fileNames = new TreeSet<String>();
      try {
      	URL url = new URL(getURIPath(zipPath));    	
      	OSPLog.finest("zip url: "+url.toExternalForm()); //$NON-NLS-1$
        BufferedInputStream bufIn = new BufferedInputStream(url.openStream());
        ZipInputStream input = new ZipInputStream(bufIn);
        ZipEntry zipEntry=null;
        while ((zipEntry=input.getNextEntry())!=null) {
        	OSPLog.finest("zip entry: "+zipEntry); //$NON-NLS-1$
          if (zipEntry.isDirectory()) continue;
          String fileName = zipEntry.getName();
          fileNames.add(fileName);
        }
        input.close();
      }
      catch (Exception ex) {}    
      return fileNames;
    }
    
    /**
     * Unzips a ZIP file into the given directory. ZIP file may be on a server.
     * Can be canceled using the static setCanceled(boolean) method.
     * Note this does not warn of possible overwrites.
     * 
     * @param zipPath the (url) path to the zip file
     * @param targetDir target directory to save the extracted files
     * @param alwaysOverwrite true to overwrite existing files, if any
     * @return the Set of extracted files
     */
    public static Set<File> unzip(String zipPath, File targetDir, boolean alwaysOverwrite) {
    	if (targetDir==null)
  			targetDir = new File(System.getProperty("java.io.tmpdir")); //$NON-NLS-1$
    	OSPLog.finer("unzipping "+zipPath+" to "+targetDir); //$NON-NLS-1$ //$NON-NLS-2$
    	try {
      	URL url = new URL(getURIPath(zipPath));    	
        BufferedInputStream bufIn = new BufferedInputStream(url.openStream());
        ZipInputStream input = new ZipInputStream(bufIn);
        ZipEntry zipEntry=null;
        Set<File> fileSet = new HashSet<File>();
        byte[] buffer = new byte[1024];
        setCanceled(false);
        while ((zipEntry=input.getNextEntry()) != null) {
          if (zipEntry.isDirectory()) continue;
          if (isCanceled()) {
            input.close();
            return null;
          }
          String filename = zipEntry.getName();
          File file = new File(targetDir, filename);
          if (!alwaysOverwrite && file.exists()) {
            fileSet.add(file);
          	continue;
          }
          file.getParentFile().mkdirs();
          int bytesRead;
          FileOutputStream output = new FileOutputStream(file);
          while ((bytesRead=input.read(buffer)) != -1) 
          	output.write(buffer, 0, bytesRead);
          output.close();
          input.closeEntry();
          fileSet.add(file);
        }
        input.close();
        return fileSet;
      }
      catch (Exception ex) { 
        ex.printStackTrace();
        return null;
      }    
    }  
    
    /**
     * Downloads a file from the web to a target File.
     * 
     * @param urlPath the path to the file
     * @param target the target file
     * @param alwaysOverwrite true to overwrite an existing file, if any
     * @return the downloaded file, or null if failed
     */
    public static File download(String urlPath, File target, boolean alwaysOverwrite) {
    	urlPath = getURIPath(urlPath);
  		target.getParentFile().mkdirs();
      if (alwaysOverwrite || !target.exists()) {
  	  	OSPLog.finer("downloading "+urlPath+" to "+target); //$NON-NLS-1$ //$NON-NLS-2$
    		try {
    			Resource res = getResourceZipURLsOK(urlPath);
  				InputStream reader = res.openInputStream();
          FileOutputStream writer = new FileOutputStream(target);
          byte[] buffer = new byte[65536]; // 2^14 = 64K
          int bytesRead = 0;
          while ((bytesRead = reader.read(buffer)) > 0) { 
          	writer.write(buffer, 0, bytesRead);
          }  
          writer.close();
          reader.close();
  			} catch (Exception ex) {
  				ex.printStackTrace();
  			} 
      }
      if (target.exists()) return target; 
      return null;
    }
    
    /**
     * Extracts a file from a ZIP archive to a target file. ZIP archive may be on a server.
     * 
     * @param source the path of the file to be extracted (eg "http:/www.server/folder/images.zip!/image1.png")
     * @param target target file to save
     * @param alwaysOverwrite true to overwrite existing files, if any
     * @return the extracted file
     */
    public static File extractFileFromZIP(String source, File target, boolean alwaysOverwrite) {
      if (!alwaysOverwrite && target.exists())
      	return target;
      String lcSource = XML.forwardSlash(source).toLowerCase();
      int n = lcSource.indexOf(".zip!/"); //$NON-NLS-1$
      if (n==-1) n = lcSource.indexOf(".jar!/");     //$NON-NLS-1$
      if (n==-1) n = lcSource.indexOf(".trz!/");     //$NON-NLS-1$
      if (n==-1) return null;
      String sourceName = source.substring(n+6);
      source = source.substring(0, n+4);
      
    	try {
      	URL url = new URL(source);    	
        BufferedInputStream bufIn = new BufferedInputStream(url.openStream());
        ZipInputStream input = new ZipInputStream(bufIn);
        ZipEntry zipEntry=null;
        byte[] buffer = new byte[1024];
  