DataProvidersManager.java

/* Copyright 2002-2018 CS Systèmes d'Information
 * Licensed to CS Systèmes d'Information (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.data;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;

/** Singleton class managing all supported {@link DataProvider data providers}.

 * <p>
 * This class is the single point of access for all data loading features. It
 * is used for example to load Earth Orientation Parameters used by IERS frames,
 * to load UTC leap seconds used by time scales, to load planetary ephemerides ...
 *
 * <p>
 * It is user-customizable: users can add their own data providers at will. This
 * allows them for example to use a database or an existing data loading library
 * in order to embed an Orekit enabled application in a global system with its
 * own data handling mechanisms. There is no upper limitation on the number of
 * providers, but often each application will use only a few.
 * </p>
 *
 * <p>
 * If the list of providers is empty when attempting to {@link #feed(String, DataLoader)
 * feed} a file loader, the {@link #addDefaultProviders()} method is called
 * automatically to set up a default configuration. This default configuration
 * contains one {@link DataProvider data provider} for each component of the
 * path-like list specified by the java property <code>orekit.data.path</code>.
 * See the {@link #feed(String, DataLoader) feed} method documentation for further
 * details. The default providers configuration is <em>not</em> set up if the list
 * is not empty. If users want to have both the default providers and additional
 * providers, they must call explicitly the {@link #addDefaultProviders()} method.
 * </p>
 *
 * <p>
 * The default configuration uses a predefined set of {@link DataFilter data filters}
 * that already handled gzip-compressed files (recognized by the {@code .gz} suffix)
 * and Unix-compressed files (recognized by the {@code .Z} suffix).
 * Users can {@link #addFilter(DataFilter) add} custom filters for handling specific
 * types of filters (decompression, deciphering...).
 * </p>
 *
 * @author Luc Maisonobe
 * @see DirectoryCrawler
 * @see ClasspathCrawler
 */
public class DataProvidersManager {

    /** Name of the property defining the root directories or zip/jar files path for default configuration. */
    public static final String OREKIT_DATA_PATH = "orekit.data.path";

    /** Supported data providers. */
    private final List<DataProvider> providers;

    /** Supported filters.
     * @since 9.2
     */
    private final List<DataFilter> filters;

    /** Number of predefined filters. */
    private final int predefinedFilters;

    /** Loaded data. */
    private final Set<String> loaded;

    /** Build an instance with default configuration.
     * <p>
     * This is a singleton, so the constructor is private.
     * </p>
     */
    private DataProvidersManager() {
        providers = new ArrayList<DataProvider>();
        filters   = new ArrayList<>();
        loaded    = new LinkedHashSet<String>();

        // set up predefined filters
        addFilter(new GzipFilter());
        addFilter(new UnixCompressFilter());

        predefinedFilters = filters.size();

    }

    /** Get the unique instance.
     * @return unique instance of the manager.
     */
    public static DataProvidersManager getInstance() {
        return LazyHolder.INSTANCE;
    }

    /** Add the default providers configuration.
     * <p>
     * The default configuration contains one {@link DataProvider data provider}
     * for each component of the path-like list specified by the java property
     * <code>orekit.data.path</code>.
     * </p>
     * <p>
     * If the property is not set or is null, no data will be available to the library
     * (for example no pole corrections will be applied and only predefined UTC steps
     * will be taken into account). No errors will be triggered in this case.
     * </p>
     * <p>
     * If the property is set, it must contains a list of existing directories or zip/jar
     * archives. One {@link DirectoryCrawler} instance will be set up for each
     * directory and one {@link ZipJarCrawler} instance (configured to look for the
     * archive in the filesystem) will be set up for each zip/jar archive. The list
     * elements in the java property are separated using the standard path separator for
     * the operating system as returned by {@link System#getProperty(String)
     * System.getProperty("path.separator")}. This standard path separator is ":" on
     * Linux and Unix type systems and ";" on Windows types systems.
     * </p>
     * @exception OrekitException if an element of the list does not exist or exists but
     * is neither a directory nor a zip/jar archive
     */
    public void addDefaultProviders() throws OrekitException {

        // get the path containing all components
        final String path = System.getProperty(OREKIT_DATA_PATH);
        if ((path != null) && !"".equals(path)) {

            // extract the various components
            for (final String name : path.split(System.getProperty("path.separator"))) {
                if (!"".equals(name)) {

                    final File file = new File(name);

                    // check component
                    if (!file.exists()) {
                        if (DataProvider.ZIP_ARCHIVE_PATTERN.matcher(name).matches()) {
                            throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, name);
                        } else {
                            throw new OrekitException(OrekitMessages.DATA_ROOT_DIRECTORY_DOES_NOT_EXIST, name);
                        }
                    }

                    if (file.isDirectory()) {
                        addProvider(new DirectoryCrawler(file));
                    } else if (DataProvider.ZIP_ARCHIVE_PATTERN.matcher(name).matches()) {
                        addProvider(new ZipJarCrawler(file));
                    } else {
                        throw new OrekitException(OrekitMessages.NEITHER_DIRECTORY_NOR_ZIP_OR_JAR, name);
                    }

                }
            }
        }

    }

    /** Add a data provider to the supported list.
     * @param provider data provider to add
     * @see #removeProvider(DataProvider)
     * @see #clearProviders()
     * @see #isSupported(DataProvider)
     * @see #getProviders()
     */
    public void addProvider(final DataProvider provider) {
        providers.add(provider);
    }

    /** Remove one provider.
     * @param provider provider instance to remove
     * @return instance removed (null if the provider was not already present)
     * @see #addProvider(DataProvider)
     * @see #clearProviders()
     * @see #isSupported(DataProvider)
     * @see #getProviders()
     * @since 5.1
     */
    public DataProvider removeProvider(final DataProvider provider) {
        for (final Iterator<DataProvider> iterator = providers.iterator(); iterator.hasNext();) {
            final DataProvider current = iterator.next();
            if (current == provider) {
                iterator.remove();
                return provider;
            }
        }
        return null;
    }

    /** Remove all data providers.
     * @see #addProvider(DataProvider)
     * @see #removeProvider(DataProvider)
     * @see #isSupported(DataProvider)
     * @see #getProviders()
     */
    public void clearProviders() {
        providers.clear();
    }

    /** Add a data filter.
     * @param filter filter to add
     * @see #applyAllFilters(NamedData)
     * @see #clearFilters()
     * @since 9.2
     */
    public void addFilter(final DataFilter filter) {
        filters.add(filter);
    }

    /** Remove all data filters, except the predefined ones.
     * @see #addFilter(DataFilter)
     * @since 9.2
     */
    public void clearFilters() {
        for (int i = filters.size() - 1; i >= predefinedFilters; --i) {
            filters.remove(i);
        }
    }

    /** Apply all the relevant data filters, taking care of layers.
     * <p>
     * If several filters can be applied, they will all be applied
     * as a stack, even recursively if required. This means that if
     * filter A applies to files with names of the form base.ext.a
     * and filter B applies to files with names of the form base.ext.b,
     * then providing base.ext.a.b.a will result in filter A being
     * applied on top of filter B which itself is applied on top of
     * another instance of filter A.
     * </p>
     * @param original original named data
     * @return fully filtered named data
     * @exception IOException if some data stream cannot be filtered
     * @see #addFilter(DataFilter)
     * @see #clearFilters()
     * @since 9.2
     */
    public NamedData applyAllFilters(final NamedData original)
        throws IOException {
        NamedData top = original;
        for (boolean filtering = true; filtering;) {
            filtering = false;
            for (final DataFilter filter : filters) {
                final NamedData filtered = filter.filter(top);
                if (filtered != top) {
                    // the filter has been applied, we need to restart the loop
                    top       = filtered;
                    filtering = true;
                    break;
                }
            }
        }
        return top;
    }

    /** Check if some provider is supported.
     * @param provider provider to check
     * @return true if the specified provider instance is already in the supported list
     * @see #addProvider(DataProvider)
     * @see #removeProvider(DataProvider)
     * @see #clearProviders()
     * @see #getProviders()
     * @since 5.1
     */
    public boolean isSupported(final DataProvider provider) {
        for (final DataProvider current : providers) {
            if (current == provider) {
                return true;
            }
        }
        return false;
    }

    /** Get an unmodifiable view of the list of supported providers.
     * @return unmodifiable view of the list of supported providers
     * @see #addProvider(DataProvider)
     * @see #removeProvider(DataProvider)
     * @see #clearProviders()
     * @see #isSupported(DataProvider)
     */
    public List<DataProvider> getProviders() {
        return Collections.unmodifiableList(providers);
    }

    /** Get an unmodifiable view of the set of data file names that have been loaded.
     * <p>
     * The names returned are exactly the ones that were given to the {@link
     * DataLoader#loadData(InputStream, String) DataLoader.loadData} method.
     * </p>
     * @return unmodifiable view of the set of data file names that have been loaded
     * @see #feed(String, DataLoader)
     * @see #clearLoadedDataNames()
     */
    public Set<String> getLoadedDataNames() {
        return Collections.unmodifiableSet(loaded);
    }

    /** Clear the set of data file names that have been loaded.
     * @see #getLoadedDataNames()
     */
    public void clearLoadedDataNames() {
        loaded.clear();
    }

    /** Feed a data file loader by browsing all data providers.
     * <p>
     * If this method is called with an empty list of providers, a default
     * providers configuration is set up. This default configuration contains
     * only one {@link DataProvider data provider}: a {@link DirectoryCrawler}
     * instance that loads data from files located somewhere in a directory hierarchy.
     * This default provider is <em>not</em> added if the list is not empty. If users
     * want to have both the default provider and other providers, they must add it
     * explicitly.
     * </p>
     * <p>
     * The providers are used in the order in which they were {@link #addProvider(DataProvider)
     * added}. As soon as one provider is able to feed the data loader, the loop is
     * stopped. If no provider is able to feed the data loader, then the last error
     * triggered is thrown.
     * </p>
     * @param supportedNames regular expression for file names supported by the visitor
     * @param loader data loader to use
     * @return true if some data has been loaded
     * @exception OrekitException if the data loader cannot be fed (read error ...)
     * or if the default configuration cannot be set up
     */
    public boolean feed(final String supportedNames, final DataLoader loader)
        throws OrekitException {

        final Pattern supported = Pattern.compile(supportedNames);

        // set up a default configuration if no providers have been set
        if (providers.isEmpty()) {
            addDefaultProviders();
        }

        // monitor the data that the loader will load
        final DataLoader monitoredLoader = new MonitoringWrapper(loader);

        // crawl the data collection
        OrekitException delayedException = null;
        for (final DataProvider provider : providers) {
            try {

                // try to feed the visitor using the current provider
                if (provider.feed(supported, monitoredLoader)) {
                    return true;
                }

            } catch (OrekitException oe) {
                // remember the last error encountered
                delayedException = oe;
            }
        }

        if (delayedException != null) {
            throw delayedException;
        }

        return false;

    }

    /** Data loading monitoring wrapper class. */
    private class MonitoringWrapper implements DataLoader {

        /** Wrapped loader. */
        private final DataLoader loader;

        /** Simple constructor.
         * @param loader loader to monitor
         */
        MonitoringWrapper(final DataLoader loader) {
            this.loader = loader;
        }

        /** {@inheritDoc} */
        public boolean stillAcceptsData() {
            // delegate to monitored loader
            return loader.stillAcceptsData();
        }

        /** {@inheritDoc} */
        public void loadData(final InputStream input, final String name)
            throws IOException, ParseException, OrekitException {

            // delegate to monitored loader
            loader.loadData(input, name);

            // monitor the fact new data has been loaded
            loaded.add(name);

        }

    }

    /** Holder for the manager singleton.
     * <p>
     * We use the Initialization on demand holder idiom to store
     * the singletons, as it is both thread-safe, efficient (no
     * synchronization) and works with all versions of java.
     * </p>
     */
    private static class LazyHolder {

        /** Unique instance. */
        private static final DataProvidersManager INSTANCE = new DataProvidersManager();

        /** Private constructor.
         * <p>This class is a utility class, it should neither have a public
         * nor a default constructor. This private constructor prevents
         * the compiler from generating one automatically.</p>
         */
        private LazyHolder() {
        }

    }

}