/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2011, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * 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 * Lesser General Public License for more details. */ package org.geotools.swing.locale; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.ListIterator; import java.util.Locale; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import org.geotools.data.DataUtilities; /** * Searches for properties files in a resource directory within the gt-swing module * and records the {@code Locales} supported by each file. This is a helper for * {@linkplain LocaleUtils}. * <p> * Normally, the {@linkplain #scan(String)} method will be responding to a call from * outside this class's jar, either directly or indirectly. An example of an indirect * outside call is when an application calls a LocaleUtils method which in turn calls * the {@code scan} method. In this case, the resource directory is searched by scanning * the relevant entries in the gt-swing jar. * <p> * For completeness, and to aid unit testing, calls from within the swing module are * also supported. In this case the resource directory is accessed as a local * {@linkplain File} object. * * @author Michael Bedward * @since 8.0 * * @source $URL$ * @version $Id$ */ public class PropertiesFileFinder { /** * Searches for properties files in the specified resource directory and returns * information about each file and the {@code Locales} that it supports. * * @param resourceDir * @return * @throws IOException */ public List<PropertiesFileInfo> scan(String resourceDir) throws IOException { List<SingleFileInfo> infoList = new ArrayList<SingleFileInfo>(); String path = getSelfPath(); if (isJarPath(path)) { JarInputStream jarFile = getAsJarFile(path); JarEntry entry; while ((entry = jarFile.getNextJarEntry()) != null) { String name = entry.getName(); if (name.startsWith(resourceDir) && name.endsWith("properties")) { infoList.add(parseEntry(resourceDir.length(), name)); } } jarFile.close(); } else { // must be running locally File localDir = getAsLocalDir(path); File[] children = localDir.listFiles(); for (File child : children) { if (child != null && child.isFile()) { String name = child.getName(); if (name.endsWith(".properties")) { infoList.add(parseEntry(0, name)); } } } } return createReturnList( infoList ); } /** * Gets the path to this class file. This will be a jar file path * if called from outside this module, or a local path if called * from within. * * @return path to this class */ private String getSelfPath() { try { String className = getClass().getSimpleName() + ".class"; URL url = getClass().getResource(className); /* * DataUtiltiies.urlToFile doesn't deal with the jar protocol * so if that's what we've got we remove the "jar:" prefix. * TODO: It would be better to add proper support to the DataUtilities * class. */ if (url.getProtocol().equals("jar")) { String urlStr = url.toExternalForm(); url = new URL(urlStr.substring(4)); } return DataUtilities.urlToFile(url).getPath(); } catch (MalformedURLException ex) { throw new RuntimeException(ex); } } /** * Tests if a path refers to a jar file. * * @param path the path * @return {@code true} if a jar file */ private boolean isJarPath(String path) { return path.contains(".jar!"); } /** * Returns a {@code JarInputStream} for the given jar. * * @param jarPath the path * @return the input stream * @throws IllegalArgumentException if the jar cannot be found * @throws IOException on error opening file */ private JarInputStream getAsJarFile(String jarPath) throws IOException { if (jarPath.startsWith("file:")) { jarPath = jarPath.substring(5); } int pos = jarPath.indexOf(".jar!"); if (pos <= 0) { throw new IllegalArgumentException("Not a valid jar path"); } jarPath = jarPath.substring(0, pos + 4); File file = new File(jarPath); if (!file.exists()) { throw new IllegalArgumentException("File not found: " + file); } return new JarInputStream(new FileInputStream(file)); } /** * Returns a {@code File} object for the given directory path. * @param dirPath the directory path * @return a new {@code File} object * @throws IllegalArgumentException if the path does not match a valid directory */ private File getAsLocalDir(String dirPath) { int pos = dirPath.lastIndexOf(File.separatorChar); File file = new File(dirPath.substring(0, pos)); if (!file.exists() || !file.isDirectory()) { throw new IllegalArgumentException("Invalid directory path: " + file); } return file; } /** * Parses an entry (either a jar file entry or local file name) and extracts * the base name and locale. * * @param prefixLength length of entry prefix to discard * @param entry the entry * @return base name and locale information */ private SingleFileInfo parseEntry(int prefixLength, String entry) { entry = entry.substring(prefixLength, entry.indexOf(".properties")); String[] parts = entry.split("_"); String baseName = parts[0]; String language = ""; String country = ""; String variant = ""; if (parts.length > 1) { language = parts[1]; } if (parts.length > 2) { country = parts[2]; } if (parts.length > 3) { variant = parts[3]; } Locale locale; if (parts.length == 1) { locale = Locale.ROOT; } else { locale = new Locale(language, country, variant); } return new SingleFileInfo(baseName, locale); } /** * Converts a list of single file information (base name plus locale) into * a list of {@linkplain PropertiesFileInfo} objects. * * @param infoList list of single file information * @return a new list of {@code PropertiesFileInfo} objects */ private List<PropertiesFileInfo> createReturnList(List<SingleFileInfo> infoList) { List<PropertiesFileInfo> pfiList = new ArrayList<PropertiesFileInfo>(); if (!infoList.isEmpty()) { Collections.sort(infoList, new Comparator<SingleFileInfo>() { @Override public int compare(SingleFileInfo o1, SingleFileInfo o2) { return o1.name.compareTo(o2.name); } }); String curName = infoList.get(0).name; List<Locale> locales = new ArrayList<Locale>(); ListIterator<SingleFileInfo> iter = infoList.listIterator(); while (iter.hasNext()) { SingleFileInfo sfi = iter.next(); if (sfi.name.equals(curName)) { locales.add(sfi.locale); } else { pfiList.add(new PropertiesFileInfo(curName, locales)); curName = sfi.name; locales.clear(); locales.add(sfi.locale); } } pfiList.add(new PropertiesFileInfo(curName, locales)); } return pfiList; } /** * Holds base name and locale for a single file. */ private static class SingleFileInfo { String name; Locale locale; public SingleFileInfo(String name, Locale locale) { this.name = name; this.locale = locale; } } }