001/** 002 * DataReaderFactory -- Factory that returns appropriate DataReader objects. 003 * 004 * Copyright (C) 2003-2025, by Joseph A. Huwaldt. All rights reserved. 005 * 006 * This library is free software; you can redistribute it and/or modify it under the terms 007 * of the GNU Lesser General Public License as published by the Free Software Foundation; 008 * either version 2 of the License, or (at your option) any later version. 009 * 010 * This library is distributed in the hope that it will be useful, but WITHOUT ANY 011 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 012 * PARTICULAR PURPOSE. See the GNU Library General Public License for more details. 013 * 014 * You should have received a copy of the GNU Lesser General Public License along with 015 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - 016 * Suite 330, Boston, MA 02111-1307, USA. Or visit: http://www.gnu.org/licenses/lgpl.html 017 */ 018package jahuwaldt.js.datareader; 019 020import java.io.*; 021import java.net.URL; 022import java.text.MessageFormat; 023import java.util.Arrays; 024import java.util.Enumeration; 025import static java.util.Objects.requireNonNull; 026import static java.util.Objects.isNull; 027import static java.util.Objects.nonNull; 028import java.util.Properties; 029import java.util.ResourceBundle; 030import javax.swing.JOptionPane; 031import javolution.util.FastTable; 032import javolution.util.FastSet; 033import javolution.util.FastComparator; 034 035/** 036 * This class returns a specific {@link DataReader} object that can read in the specified 037 * file. This class implements a pluggable architecture. A new {@link DataReader} class 038 * can be added by simply creating a subclass of <code>DataReader</code>, creating a 039 * datareader.properties file that refers to it, and putting that properties file 040 * somewhere in the Java search path. 041 * 042 * <p> Modified by: Joseph A. Huwaldt </p> 043 * 044 * @author Joseph A. Huwaldt, Date: March 25, 2003 045 * @version February 23, 2025 046 */ 047public final class DataReaderFactory { 048 049 /** 050 * The resource bundle for this package. 051 */ 052 public static final ResourceBundle RESOURCES = DataReader.RESOURCES; 053 054 /** 055 * All class loader resources with this name ("datareader.properties") are loaded as 056 * .properties definitions and merged together to create a global list of reader 057 * handler mappings. 058 */ 059 private static final String MAPPING_RES_NAME = RESOURCES.getString("mappingResourcePath"); 060 061 // An array containing a reference to all the readers that have been found. 062 private static final DataReader[] _allReaders; // set in <clinit> 063 064 // Locate and load all the readers we can find. 065 static { 066 DataReader[] temp = null; 067 try { 068 temp = loadResourceList(MAPPING_RES_NAME, getClassLoader()); 069 070 } catch (Exception e) { 071 System.out.println("could not load all [" + MAPPING_RES_NAME + "] mappings:"); 072 e.printStackTrace(); 073 } 074 075 _allReaders = temp; 076 } 077 078 // this class is not extendible 079 private DataReaderFactory() { } 080 081 /** 082 * Method that attempts to find a {@link DataReader} object that might be able to read 083 * the data in the specified input stream. 084 * 085 * @param inputStream An input stream to the file that a reader is to be returned for. 086 * @param pathName The path or file name for the file that a reader is to be 087 * returned for. 088 * @return A reader that is appropriate for the specified file, or <code>null</code> 089 * if the user cancels the multiple reader selection dialog. 090 * @throws IOException if an appropriate reader for the file could not be found. 091 */ 092 public static DataReader getReader(BufferedInputStream inputStream, String pathName) throws IOException { 093 String name = new File(pathName).getName(); 094 095 // Get the list of data readers that are available. 096 DataReader[] allReaders = getAllReaders(); 097 if (isNull(allReaders) || allReaders.length < 1) 098 throw new IOException(RESOURCES.getString("noReadersErr")); 099 100 FastTable<DataReader> list = FastTable.newInstance(); 101 102 try { 103 // Loop through all the available readers and see if any of them will work. 104 for (DataReader reader : allReaders) { 105 106 // Mark the current position in the input stream (the start of the stream). 107 inputStream.mark(1024000); 108 109 reader.setDefaultSetName(name); 110 111 // Can this reader read the specified input stream? 112 int canReadFile = reader.canReadData(pathName, inputStream); 113 114 // If the reader is certain it can read the data, use it. 115 if (canReadFile == DataReader.YES) { 116 FastTable.recycle(list); 117 return reader; 118 } 119 120 // Otherwise, build a list of maybes. 121 if (canReadFile == DataReader.MAYBE) 122 list.add(reader); 123 124 // Return to the start of the stream for the next pass. 125 inputStream.reset(); 126 } 127 128 } catch (Exception e) { 129 FastTable.recycle(list); 130 e.printStackTrace(); 131 throw new IOException(MessageFormat.format(RESOURCES.getString("fileTypeErr"),name)); 132 } 133 134 if (list.size() < 1) { 135 FastTable.recycle(list); 136 throw new IOException(MessageFormat.format(RESOURCES.getString("unknownFileTypeErr"),name)); 137 } 138 139 // If there is only one reader in the list, try and use it. 140 DataReader selectedReader; 141 if (list.size() == 1) 142 selectedReader = list.get(0); 143 else { 144 // Ask the user to select which reader they want to try and use. 145 DataReader[] possibleValues = list.toArray(new DataReader[list.size()]); 146 selectedReader = (DataReader)JOptionPane.showInputDialog(null, 147 MessageFormat.format(RESOURCES.getString("selFormatMsg"),name), 148 RESOURCES.getString("selFormatTitle"),JOptionPane.INFORMATION_MESSAGE, 149 null, possibleValues, possibleValues[0]); 150 } 151 152 // Clean up before leaving. 153 FastTable.recycle(list); 154 155 return selectedReader; 156 } 157 158 /** 159 * Method that attempts to find an {@link DataReader} object that might be able to 160 * read the data in the specified file. 161 * 162 * @param theFile The file to find a reader for. 163 * @return A reader that is appropriate for the specified file, or <code>null</code> 164 * if the user cancels the multiple reader selection dialog. 165 * @throws IOException if an appropriate reader for the file could not be found. 166 */ 167 @SuppressWarnings("null") 168 public static DataReader getReader(File theFile) throws IOException { 169 170 if (!theFile.exists()) 171 throw new IOException(MessageFormat.format( 172 RESOURCES.getString("fileNotFoundErr"),theFile.getName())); 173 174 DataReader reader = null; 175 BufferedInputStream input = null; 176 try { 177 // Get an input stream to the file. 178 input = new BufferedInputStream(new FileInputStream(theFile)); 179 String pathName = theFile.getPath(); 180 181 reader = getReader(input, pathName); 182 183 } finally { 184 // Make sure and close the input stream. 185 if (nonNull(input)) 186 input.close(); 187 } 188 189 return reader; 190 } 191 192 /** 193 * Method that returns a list of all the DataReader objects found by this 194 * factory. 195 * 196 * @return An array of DataReader objects (can be null if static initialization 197 * failed). 198 */ 199 public static DataReader[] getAllReaders() { 200 return _allReaders; 201 } 202 203 /** 204 * Loads a reader list that is a union of *all* resources named 'resourceName' as seen 205 * by 'loader'. Null 'loader' is equivalent to the application loader. 206 */ 207 @SuppressWarnings("null") 208 private static DataReader[] loadResourceList(final String resourceName, ClassLoader loader) { 209 if (isNull(loader)) 210 loader = ClassLoader.getSystemClassLoader(); 211 212 final FastSet<DataReader> result = FastSet.newInstance(); 213 214 try { 215 // NOTE: using getResources() here 216 final Enumeration<URL> resources = loader.getResources(resourceName); 217 218 if (nonNull(resources)) { 219 // merge all mappings in 'resources': 220 221 while (resources.hasMoreElements()) { 222 final URL url = resources.nextElement(); 223 final Properties mapping; 224 225 InputStream urlIn = null; 226 try { 227 urlIn = url.openStream(); 228 229 mapping = new Properties(); 230 mapping.load(urlIn); // load in .properties format 231 232 } catch (IOException ioe) { 233 // ignore this resource and go to the next one 234 continue; 235 236 } finally { 237 if (nonNull(urlIn)) 238 try { 239 urlIn.close(); 240 } catch (Exception ignore) { 241 } 242 } 243 244 // load all readers specified in 'mapping': 245 for (Enumeration<?> keys = mapping.propertyNames(); keys.hasMoreElements();) { 246 final String format = (String)keys.nextElement(); 247 final String implClassName = mapping.getProperty(format); 248 249 result.add(loadResource(implClassName, loader)); 250 } 251 } 252 } 253 254 } catch (IOException ignore) { 255 // ignore: an empty result will be returned 256 } 257 258 // Convert result Set to an array. 259 DataReader[] resultArr = result.toArray(new DataReader[result.size()]); 260 261 // Sort the array using the specified comparator. 262 Arrays.sort(resultArr, FastComparator.DEFAULT); 263 264 // Clean up before leaving. 265 FastSet.recycle(result); 266 267 // Output the sorted array. 268 return resultArr; 269 } 270 271 /** 272 * Loads and initializes a single resource for a given format name via a given 273 * classloader. For simplicity, all errors are converted to RuntimeExceptions. 274 */ 275 private static DataReader loadResource(final String className, final ClassLoader loader) { 276 requireNonNull(className, MessageFormat.format(RESOURCES.getString("paramNullErr"), "className")); 277 requireNonNull(loader, MessageFormat.format(RESOURCES.getString("paramNullErr"), "loader")); 278 279 final Class<?> cls; 280 final Object reader; 281 try { 282 cls = Class.forName(className, true, loader); 283 reader = cls.getDeclaredConstructor().newInstance(); 284 285 } catch (Exception e) { 286 throw new RuntimeException(MessageFormat.format( 287 RESOURCES.getString("classLoadErr"),className),e); 288 } 289 290 if (!(reader instanceof DataReader)) 291 throw new RuntimeException(MessageFormat.format( 292 RESOURCES.getString("classTypeErr"),cls.getName())); 293 294 return (DataReader)reader; 295 } 296 297 /** 298 * This method decides on which class loader is to be used by all resource/class 299 * loading in this class. At the very least you should use the current thread's 300 * context loader. A better strategy would be to use techniques shown in 301 * http://www.javaworld.com/javaworld/javaqa/2003-06/01-qa-0606-load.html 302 */ 303 private static ClassLoader getClassLoader() { 304 return Thread.currentThread().getContextClassLoader(); 305 } 306}