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}