/** * Copyright (C) 2002-2012 The FreeCol Team * * This file is part of FreeCol. * * FreeCol is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * FreeCol 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with FreeCol. If not, see <http://www.gnu.org/licenses/>. */ package net.sf.freecol.common.io; import java.io.BufferedInputStream; import java.io.File; import java.io.FileFilter; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.URLConnection; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.Locale; import java.util.Properties; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.logging.Level; import java.util.logging.Logger; import net.sf.freecol.common.resources.Resource; import net.sf.freecol.common.resources.ResourceFactory; import net.sf.freecol.common.resources.ResourceMapping; import net.sf.freecol.common.util.Utils; /** * Support for reading a FreeCol data file. The data file * is either a ZIP-file or a directory containing certain files. */ public class FreeColDataFile { private static final Logger logger = Logger.getLogger(FreeColDataFile.class.getName()); private static final String FILE_PREFIX = "resources"; private static final String FILE_SUFFIX = ".properties"; /** A fake URI scheme for resources delegating to other resources. */ private static final String resourceScheme = "resource:"; /** The file this object represents. */ private final File file; /** * A prefix string for the jar-entries (only if {@link #file} is * a ZIP-file). */ private final String jarDirectory; /** * Opens the given file for reading. * * @param file The file to be read. */ public FreeColDataFile(File file) { if (!file.exists()) { for (String ending : getFileEndings()) { final File tempFile = new File(file.getAbsolutePath() + ending); if (tempFile.exists()) { file = tempFile; break; } } } this.file = file; if (file.isDirectory()) { this.jarDirectory = null; } else { this.jarDirectory = findJarDirectory(file.getName().substring(0, file.getName().lastIndexOf('.')), file); } } /** * Finds the directory within the zip-file in case the data file * has been renamed. * * @param expectedName The name the directory should have. * @param file The zip-file. * @return The name of the base directory in the zip-file. */ private static String findJarDirectory(final String expectedName, File file) { JarFile jf = null; try { jf = new JarFile(file); final JarEntry entry = jf.entries().nextElement(); final String en = entry.getName(); final int index = en.lastIndexOf('/'); String name = ""; if (index > 0) { name = en.substring(0, index + 1); } return name; } catch (Exception e) { logger.log(Level.WARNING, "Exception while reading data file.", e); return expectedName; } finally { try { jf.close(); } catch (Exception e) {} } } /** * Returns a list containing the names of all * message files to load. * * @param prefix a <code>String</code> value * @param suffix a <code>String</code> value * @param language a <code>String</code> value * @param country a <code>String</code> value * @param variant a <code>String</code> value * @return a <code>List<String></code> value */ public static List<String> getFileNames(String prefix, String suffix, String language, String country, String variant) { List<String> result = new ArrayList<String>(4); if (!language.equals("")) { language = "_" + language; } if (!country.equals("")) { country = "_" + country; } if (!variant.equals("")) { variant = "_" + variant; } result.add(prefix + suffix); String filename = prefix + language + suffix; if (!result.contains(filename)) result.add(filename); filename = prefix + language + country + suffix; if (!result.contains(filename)) result.add(filename); filename = prefix + language + country + variant + suffix; if (!result.contains(filename)) result.add(filename); return result; } /** * Returns an input stream for the specified resource. * @param filename The filename of a resource within this collection of * data. If this object represents a directory then the provided filename * should be relative towards the path of the directory. In case * of a compressed archive it should be the path within the * archive. * @return an <code>InputStream</code> value * @exception IOException if an error occurs */ public BufferedInputStream getInputStream(String filename) throws IOException { final URLConnection connection = getURI(filename).toURL().openConnection(); connection.setDefaultUseCaches(false); return new BufferedInputStream(connection.getInputStream()); } protected URI getURI(String filename) { try { if (filename.startsWith("urn:")) { try { return new URI(filename); } catch (URISyntaxException e) { logger.log(Level.WARNING, "Resource creation failure with |" + filename + "|", e); return null; } } else if (file.isDirectory()) { return new File(file, filename).toURI(); } else { return new URI("jar:file", file + "!/" + jarDirectory + filename, null); } } catch (Exception e) { logger.log(Level.WARNING, "Failed to lookup: " + filename + " in: " + file, e); return null; } } /** * Creates a <code>ResourceMapping</code> from the available * resource files. * * @return A <code>ResourceMapping</code> or <code>null</code> * there is no resource mapping file. */ public ResourceMapping getResourceMapping() { final Properties properties = new Properties(); Locale locale = Locale.getDefault(); for (String fileName : getFileNames(FILE_PREFIX, FILE_SUFFIX, locale.getLanguage(), locale.getCountry(), locale.getVariant())) { try { final InputStream is = getInputStream(fileName); try { properties.load(is); logger.info("Loaded ResourceMapping " + fileName + " from " + file + "."); } finally { try { is.close(); } catch (Exception e) {} } } catch (FileNotFoundException e) { logger.finest("No ResourceMapping " + fileName + " in " + file + "."); } catch (IOException e) { logger.log(Level.WARNING, "Exception while reading ResourceMapping from: " + file, e); return null; } } ResourceMapping rc = new ResourceMapping(); List<String> todo = new ArrayList<String>(); Enumeration<?> pn = properties.propertyNames(); while (pn.hasMoreElements()) { final String key = (String) pn.nextElement(); final String value = properties.getProperty(key); if (value.startsWith(resourceScheme)) { todo.add(key); } else { URI uri = getURI(value); if (uri != null) { Resource r = ResourceFactory.createResource(uri); rc.add(key, r); if (r == null) { System.err.println("Failed to load resource " + uri.toString()); } } } } boolean progress = true; List<String> miss = new ArrayList<String>(); while (progress && !todo.isEmpty()) { miss.clear(); progress = false; while (!todo.isEmpty()) { final String key = todo.remove(0); final String value = properties.getProperty(key) .substring(resourceScheme.length()); Resource r = rc.get(value); if (r == null) { miss.add(key); } else { rc.add(key, r); progress = true; } } todo.addAll(miss); } if (!todo.isEmpty()) { logger.warning("Could not resolve virtual resource/s: " + Utils.join(" ", todo)); } return rc; } /** * Returns a <code>FileFilter</code>. * @return The <code>FileFilter</code>. */ public FileFilter getFileFilter() { return new FileFilter() { public boolean accept(File f) { final String name = f.getName(); for (String ending : getFileEndings()) { if (name.endsWith(ending)) { return true; } } return false; } }; } /** * File endings that are supported for this type of data file. * @return An array with a single element: ".zip". */ protected String[] getFileEndings() { return new String[] {".zip"}; } }