/**
* 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"};
}
}