/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF 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
///////////////
package org.apache.jena.ontology;


// Imports
///////////////
import java.io.InputStream ;
import java.util.* ;

import org.apache.jena.ontology.impl.OntResolve;
import org.apache.jena.ontology.models.ModelMaker;
import org.apache.jena.rdf.model.* ;
import org.apache.jena.shared.JenaException ;
import org.apache.jena.shared.PrefixMapping ;
import org.apache.jena.shared.impl.PrefixMappingImpl ;
import org.apache.jena.util.FileManager ;
import org.apache.jena.util.FileUtils ;
import org.apache.jena.vocabulary.OntDocManagerVocab ;
import org.apache.jena.vocabulary.RDF ;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;


/**
 * <p>
 * Provides services for managing ontology documents, including loading imported
 * documents, and locally caching documents from resolvable URL's to improve
 * load performance. This class now delegates some of the responsibility for
 * resolving URI's and caching models to {@link org.apache.jena.util.FileManager FileManager}.
 * By default, OntDocumentManager will use a copy of the
 * singleton global FileManager. Alternatively, a specific <code>FileManager</code>
 * can be given to a document manager instance to use when resolving URI's and file paths.
 * Note that the default behaviour is to hold a <strong>copy</strong> of the global file
 * manager. In order to ensure that the document manager directly uses the global
 * file manager (e.g. so that document manager sees updates to the location mappings
 * held by the file manager), use the {@link #setFileManager(FileManager)} method. For
 * example:
 * </p>
 * <pre>OntDocumentManager dm = OntDocumentManager.getInstance();
 * dm.setFileManager( FileManager.get() );
 * </pre>
 */
@SuppressWarnings({"deprecation", "removal"})
public class OntDocumentManager
{
    // @SuppressWarnings - FileManager model caching.

    // Constants
    ////////////////////////////////////

    /** The default path for searching for the metadata on locally cached ontologies */
    public static final String DEFAULT_METADATA_PATH = "file:ont-policy.rdf;file:etc/ont-policy.rdf;ont-policy.rdf";

    /** Namespace for ontology metadata resources and properties */
    public static final String NS = "http://jena.hpl.hp.com/schemas/2003/03/ont-manager#";

    /** The anchor char is added to the end of namespace prefix expansions */
    public static final String ANCHOR = "#";

    /** rdf:type for ontology specification nodes in meta-data file */
    public static final Resource ONTOLOGY_SPEC = OntDocManagerVocab.OntologySpec;

    /** Represents the public URI of an ontology; also used to derive the namespace */
    public static final Property PUBLIC_URI = OntDocManagerVocab.publicURI;

    /** Represents the alternative local copy of the public ontology; assumed to be resolvable, hence URL not URI */
    public static final Property ALT_URL = OntDocManagerVocab.altURL;

    /** Represents the standard prefix for this namespace */
    public static final Property PREFIX = OntDocManagerVocab.prefix;

    /** Represents the ontology language used to encode the ontology */
    public static final Property LANGUAGE = OntDocManagerVocab.language;

    /** rdf:type for document manager policy nodes */
    public static final Resource DOC_MGR_POLICY = OntDocManagerVocab.DocumentManagerPolicy;

    /** Defines boolean policy choice of caching loaded models */
    public static final Property CACHE_MODELS = OntDocManagerVocab.cacheModels;

    /** Defines boolean policy choice of loading the imports closure */
    public static final Property PROCESS_IMPORTS = OntDocManagerVocab.processImports;

    /** Specifies the URI of an ontology that we do not want to import, even if processImports is true. */
    public static final Property IGNORE_IMPORT = OntDocManagerVocab.ignoreImport;

    /** The policy property for including the pre-declared namespace prefixes in a model. */
    public static final Property USE_DECLARED_NS_PREFIXES = OntDocManagerVocab.useDeclaredNsPrefixes;


    // Static variables
    //////////////////////////////////

    /** Default document manager instance */
    private static OntDocumentManager s_instance = null;

    /** Logger for this class */
    private static Logger log = LoggerFactory.getLogger( OntDocumentManager.class );


    // Instance variables
    //////////////////////////////////

    /** The search path for metadata */
    protected String m_searchPath = DEFAULT_METADATA_PATH;

    /** FileManager instance that provides location resolution service - defaults to global instance */
    protected FileManager m_fileMgr;

    /** Flag to indicate we're using a copy of the global file manager */
    protected boolean m_usingGlobalFileMgr = false;

    /** Flag: process the imports closure */
    protected boolean m_processImports = true;

    /** List of URI's that will be ignored when doing imports processing */
    protected Set<String> m_ignoreImports = new HashSet<>();

    /** Default prefix mapping to use to seed all models */
    protected PrefixMapping m_prefixMap = new PrefixMappingImpl();

    /** Flag to control whether we include the standard prefixes in generated models - default true. */
    protected boolean m_useDeclaredPrefixes = true;

    /** The URL of the policy file that was loaded, or null if no external policy file has yet been loaded */
    protected String m_policyURL;

    /** Optional handler for failed read */
    protected ReadFailureHandler m_rfHandler;

    /** Read hook that can intercept the process of reading a file or URL */
    protected ReadHook m_readHook = new DefaultReadHook();


    // Constructors
    //////////////////////////////////

    /**
     * <p>
     * Initialise a document manager by searching the default path for ontology
     * metadata about known ontologies cached locally.
     * </p>
     */
    public OntDocumentManager() {
        this( DEFAULT_METADATA_PATH );
    }


    /**
     * <p>
     * Initialise a document manager by searching the given path for ontology
     * metadata about known ontologies cached locally.
     * </p>
     *
     * @param path The search path to search for initial metadata, which will
     * also replace the current search path for this document manager.  Use
     * null to prevent loading of any initial ontology metadata. The path is a series
     * of URL's, separated by a semi-colon (;).
     */
    public OntDocumentManager( String path ) {
        this( null, path );
    }


    /**
     * <p>
     * Initialise a document manager by with the given FileManager, and
     * then searching the given path for ontology
     * metadata about known ontologies cached locally.
     * </p>
     *
     * @param path The search path to search for initial metadata
     * @see #OntDocumentManager(String)
     */
    public OntDocumentManager( FileManager fileMgr, String path ) {
        setFileManager( fileMgr );
        setDefaults();
        m_searchPath = (path == null) ? "" : path;
        initialiseMetadata( m_searchPath );
    }


    /**
     * <p>Initialise a document manager with the given configuration model. This model
     * is used in place of any model that might be
     * found by searching the meta-data search path. Uses the default file manager,
     * i.e. a copy of the global file manager.</p>
     * @param config An RDF model containing configuration information for this document manager.
     */
    public OntDocumentManager( Model config ) {
        this( null, config );
    }


    /**
     * <p>Initialise a document manager with the given configuration model. This model
     * is used in place of any model that might be
     * found by searching the meta-data search path.</p>
     * @param fileMgr A file manager to use when locating source files for ontologies
     * @param config An RDF model containing configuration information for this document manager.
     */
    public OntDocumentManager( FileManager fileMgr, Model config ) {
        // we don't need to reset first since this is a new doc mgr
        setFileManager( fileMgr );
        setDefaults();
        configure( config, false );
    }


    // External signature methods
    //////////////////////////////////

    /**
     * <p>
     * OntDocumentManager is not a singleton, but a global default instance is available
     * for applications where a single shared document manager is sufficient.
     * </p>
     *
     * @return The default, global instance of a document manager
     */
    public static OntDocumentManager getInstance() {
        if (s_instance == null) {
            s_instance = new OntDocumentManager();
        }
        return s_instance;
    }


    /**
     * <p>Answer the file manager instance being used by this document manager.</p>
     * @return This object's file manager
     */
    public FileManager getFileManager() {
        return m_fileMgr;
    }


    /**
     * Replace the existing ReadHook with the given value. The previous read
     * hook is returned.
     * @param hook The new read hook
     * @return The old read hook
     * @exception IllegalArgumentException if the new read hook is null
     */
    public ReadHook setReadHook( ReadHook hook ) {
        if (hook == null) {
            throw new IllegalArgumentException( "ReadHook cannot be null" );
        }
        ReadHook rh = m_readHook;
        m_readHook = hook;
        return rh;
    }

    /**
     * Answer the current ReadHook for this document manager instance
     * @return The read hook
     */
    public ReadHook getReadHook() {
        return m_readHook;
    }

    /**
     * <p>Set the file manager used by this ODM instance to <strong>a
     * copy</strong> of the global file manager (and, by extension, the
     * global location mapper).</p>
     */
    public void setFileManager() {
        setFileManager( FileManager.getInternal().clone() ) ;
        m_usingGlobalFileMgr = true;
    }

    /**
     * <p>Set the file manager used by this ODM instance to <strong>a
     * copy</strong> of the global file manager (and, by extension, the
     * global location mapper).</p>
     * @param fileMgr The new file manager
     */
    public void setFileManager( FileManager fileMgr ) {
        if (fileMgr == null) {
            // use default fm
            setFileManager();
        }
        else {
            m_fileMgr = fileMgr;
            m_usingGlobalFileMgr = false;
        }
    }


    /**
     * <p>
     * Answer the path used to search for the ontology metadata to load. The format is
     * a ';' separated list of URI's. The first URI on the path that is readable is
     * taken to be the location of the local ontology metadata.
     * </p>
     *
     * @return The ontology metadata search path, as a string.
     */
    public String getMetadataSearchPath() {
        return m_searchPath;
    }


    /**
     * <p>
     * Change the search path for loading ontology metadata to the given path. If
     * <code>replace</code> is true, any existing mappings are removed before the
     * new path is searched.  Otherwise, existing data will only be replaced if
     * it is clobbered by keys loaded from the metadata loaded from the new path.
     * </p>
     *
     * @param path The new metadata search path (see {@link #getMetadataSearchPath} for format)
     * @param replace If true, clear existing mappings first
     */
    public void setMetadataSearchPath( String path, boolean replace ) {
        if (replace) {
            reset();
        }
        m_searchPath = path;
        m_policyURL = null;
        initialiseMetadata( path );
    }


    /**
     * Set the handler for read failures, overwriting any existing value.
     * @param rfHandler The new handler for failed document read attempts.
     */
    public void setReadFailureHandler( ReadFailureHandler rfHandler ) {
        m_rfHandler = rfHandler;
    }

    /**
     * Answer the handler object that handles failed document read attempts,
     * or null if not defined.
     * @return The current read failure handler, or null
     */
    public ReadFailureHandler getReadFailureHandler() {
        return m_rfHandler;
    }


    /**
     * <p>Configure this document manager using the given configuration information, after
     * first resetting the model back to all default values.</p>
     * @param config Document manager configuration as an RDF model
     * @see #configure( Model, boolean )
     */
    public void configure( Model config ) {
        configure( config, true );
    }


    /**
     * <p>Configure this document manager according to the configuration options
     * supplied by the given configuration model. If <code>reset</code> is true, the
     * document manager is first reset back to all default values.</p>
     * @param config Document manager configuration as an RDF model
     * @param reset If true, reset the document manager to default values, before
     * attempting to configure the document manager using the given model.
     * @see #reset
     */
    public void configure( Model config, boolean reset ) {
        if (reset) {
            reset( false );
        }

        processMetadata( config );
    }


    /**
     * <p>Reset all state in this document manager back to the default
     * values it would have had when the object was created. Optionally
     * reload the profile metadata from the search path. <strong>Note</strong>
     * that the metadata search path is not changed by this reset.</p>
     * @param reload If true, reload the configuration file from the
     * search path.
     */
    public void reset( boolean reload ) {
        // first check if we are using the global file manager, or a local one
        if (m_usingGlobalFileMgr) {
            // we can do a general reset by throwing away the old FM and creating a new one
            setFileManager();
        }
        else {
            // not using the global default FM, so we reset to best effort
            getFileManager().resetCache();
        }

        m_ignoreImports.clear();
        m_prefixMap = new PrefixMappingImpl();

        setDefaults();

        if (reload) {
            initialiseMetadata( m_searchPath );
        }
    }

    /**
     * <p>Reset all state in this document manager back to the default
     * values it would have had when the object was created. This does
     * <strong>not</strong> reload the configuration information from
     * the search path.  Note also that the metadata search path is one
     * of the values that is reset back to its default value.</p>
     * @see #reset( boolean )
     */
    public void reset() {
        reset( false );
    }


    /**
     * <p>
     * Answer an iterator over the ontology document URI's that this document manager
     * knows to re-direct to other URL's. <strong>Note</strong> that being in this
     * iteration does <em>not</em> mean that a document with the given name is in
     * the set of cached models held by this document manager.
     * </p>
     *
     * @return An Iterator ranging over the public URI strings for the known
     * document URI's.
     */
    public Iterator<String> listDocuments() {
        return getFileManager().getLocationMapper().listAltEntries();
    }


    /**
     * <p>
     * Answer the URL of the alternative copy of the ontology document with the given URI, if known,
     * or the URI unchanged if no alternative is known.
     * </p>
     *
     * @param uri The ontology document to lookup
     * @return The resolvable location of the alternative copy, if known, or <code>uri</code> otherwise
     */
    public String doAltURLMapping( String uri ) {
        return getFileManager().mapURI( uri );
    }


    /**
     * <p>
     * Answer the cached model corresponding to the given document, if known.
     * </p>
     *
     * @param uri The ontology document to lookup
     * @return The model for the document, or null if the model is not known.
     * @see #getOntology
     */
    public Model getModel( String uri ) {
        uri = OntResolve.resolve(uri);
        Model m = getFileManager().getFromCache( uri );

        // if a previously cached model has been closed, we ignore it
        if (m != null && m.isClosed()) {
            getFileManager().removeCacheModel( uri );
            m = null;
        }

        return m;
    }


    /**
     * <p>
     * Add an entry for an alternative copy of the document with the given document
     * URI.
     * </p>
     *
     * @param docURI The public URI of the ontology document
     * @param locationURL A locally resolvable URL where an alternative copy of the
     *         ontology document can be found
     */
    public void addAltEntry( String docURI, String locationURL ) {
        getFileManager().getLocationMapper().addAltEntry( docURI, locationURL );
    }


    /**
     * <p>
     * Add an entry that <code>model</code> is the appropriate model to use
     * for the given ontology document. Will not replace any existing
     * model that is cached for this URI (see
     * {@link #addModel(String, Model, boolean)} for an alternative
     * that can replace existing models).
     * </p>
     *
     * @param docURI The public URI of the ontology document
     * @param model A model containing the triples from the document
     */
    public void addModel( String docURI, Model model ) {
        addModel( docURI, model, false );
    }


    /**
     * <p>
     * Add an entry that <code>model</code> is the appropriate model to use
     * for the given ontology document
     * </p>
     *
     * @param docURI The public URI of the ontology document
     * @param model A model containing the triples from the document
     * @param replace If true, replace any existing entry with this one.
     */
    public void addModel( String docURI, Model model, boolean replace ) {
        docURI = OntResolve.resolve(docURI);

        if (getFileManager().isCachingModels() &&
            (replace || !getFileManager().hasCachedModel( docURI )))
        {
            getFileManager().addCacheModel( docURI, model );
        }
    }


    /**
     * <p>
     * Remove all managed entries for the given document.  Note does not side-effect
     * the prefixes table: this will have to be done separately.
     * </p>
     *
     * @param docURI The public URI for an ontology document
     */
    public void forget( String docURI ) {
        docURI = OntResolve.resolve(docURI);
        getFileManager().getLocationMapper().removeAltEntry( docURI );
        getFileManager().removeCacheModel( docURI );
    }


    /**
     * <p>
     * Answer the ontology model that results from loading the document with the
     * given URI.  This may be a cached model, if this document manager's policy
     * is to cache loaded models.  If not, or if no model is cached, the document
     * will be read into a suitable model.  The model will contain the imports closure
     * of the ontology, if that is the current policy of this document manager.
     * </p>
     *
     * @param uri Identifies the model to load.
     * @param spec Specifies the structure of the ontology model to create
     * @return An ontology model containing the statements from the ontology document.
     * @see #getModel
     */
    public OntModel getOntology( String uri, OntModelSpec spec ) {
        // ensure consistency of document managers (to allow access to cached documents)
        OntModelSpec _spec = spec;
        if (_spec.getDocumentManager() != this) {
            _spec = new OntModelSpec( spec );
            _spec.setDocumentManager( this );
        }

        // cached already?
        if (getFileManager().hasCachedModel( uri )) {
            Model cached = getFileManager().getFromCache( uri );
            if (cached instanceof OntModel) {
                return (OntModel) cached;
            }
            else {
                return ModelFactory.createOntologyModel( _spec, cached );
            }
        }
        else {
            OntModel m = ModelFactory.createOntologyModel( _spec, null );
            read( m, uri, true );

            // cache this model for future re-use
            addModel( uri, m );
            return m;
        }
    }


    /**
     * <p>
     * Answer the policy flag indicating whether the imports statements of
     * loaded ontologies will be processed to build a union of s.
     * </p>
     *
     * @return True if imported models will be included in a loaded model
     */
    public boolean getProcessImports() {
        return m_processImports;
    }


    /**
     * <p>
     * Answer true if the models loaded by this document manager from a given
     * URI will be cached, so that they can be re-used in other compound
     * ontology models.
     * </p>
     *
     * @return If true, a cache is maintained of loaded models from their URI's.
     */
    public boolean getCacheModels() {
        return getFileManager().isCachingModels();
    }


    /**
     * <p>
     * Set the policy flag for processing imports of loaded ontologies.
     * </p>
     *
     * @param processImports If true, load imported ontologies during load
     * @see #getProcessImports
     */
    public void setProcessImports( boolean processImports ) {
        m_processImports = processImports;
    }


    /**
     * <p>
     * Set the policy flag that indicates whether loaded models are cached by URI
     * </p>
     *
     * @param cacheModels If true, models will be cached by URI
     * @see #getCacheModels()
     */
    public void setCacheModels( boolean cacheModels ) {
        getFileManager().setModelCaching( cacheModels );
    }

    /**
     * <p>Add the given URI to the set of URI's we ignore in imports statements</p>
     * @param uri A URI to ignore when importing
     */
    public void addIgnoreImport( String uri ) {
        uri = OntResolve.resolve(uri);
        m_ignoreImports.add( uri );
    }

    /**
     * <p>Remove the given URI from the set of URI's we ignore in imports statements</p>
     * @param uri A URI to ignore no longer when importing
     */
    public void removeIgnoreImport( String uri ) {
        uri = OntResolve.resolve(uri);
        m_ignoreImports.remove( uri );
    }

    /**
     * <p>Answer an iterator over the set of URI's we're ignoring</p>
     * @return An iterator over ignored imports
     */
    public Iterator<String> listIgnoredImports() {
        return m_ignoreImports.iterator();
    }

    /**
     * <p>Answer true if the given URI is one that will be ignored during imports </p>
     * @param uri A URI to test
     * @return True if uri will be ignored as an import
     */
    public boolean ignoringImport( String uri ) {
        uri = OntResolve.resolve(uri);
        return m_ignoreImports.contains( uri );
    }

    /**
     * <p>
     * Remove all entries from the model cache
     * </p>
     */
    public void clearCache() {
        getFileManager().resetCache();
    }


    /**
     * <p>
     * Inspect the statements in the graph expressed by the given model, and load
     * into the model any imported documents.  Imports statements are recognised according
     * to the model's language profile.  An occurs check allows cycles of importing
     * safely.  This method will do nothing if the {@linkplain #getProcessImports policy}
     * for this manager is not to process imports.  If the {@linkplain #getCacheModels cache policy}
     * for this doc manager allows, models will be cached by URI and re-used where possible.
     * </p>
     *
     * @param model An ontology model whose imports are to be loaded.
     */
    public void loadImports( OntModel model ) {
        if (m_processImports) {
            List<String> readQueue = new ArrayList<>();

            // add the imported statements from the given model to the processing queue
            queueImports( model, readQueue, model.getProfile() );
            loadImports( model, readQueue );
        }
    }


    /**
     * <p>Add the given model from the given URI as an import to the given model.  Any models imported by the given
     * URI will also be imported.</p>
     *
     * @param model A model to import into
     * @param uri The URI of a document to import
     */
    public void loadImport( OntModel model, String uri ) {
        if (m_processImports) {
            List<String> readQueue = new ArrayList<>();
            readQueue.add( uri );
            loadImports( model, readQueue );
        }
    }


    /**
     * <p>Remove from the given model the import denoted by the given URI.</p>
     *
     * @param model A model
     * @param uri The URI of a document to no longer import
     */
    public void unloadImport( OntModel model, String uri ) {
        if (m_processImports) {
            List<String> unloadQueue = new ArrayList<>();
            unloadQueue.add( uri );
            unloadImports( model, unloadQueue );
        }
    }


    /**
     * <p>Answer the URL of the most recently loaded policy URL, or null
     * if no document manager policy has yet been loaded since the metadata
     * search path was last set.</p>
     * @return The most recently loaded policy URL or null.
     */
    public String getLoadedPolicyURL() {
        return m_policyURL;
    }


    // Internal implementation methods
    //////////////////////////////////

    /**
     * <p>Load all of the imports in the queue</p>
     * @param model The model to load the imports into
     * @param readQueue The queue of imports to load
     */
    protected void loadImports( OntModel model, List<String> readQueue ) {
        while (!readQueue.isEmpty()) {
            // we process the import statements as a FIFO queue
            String importURI = readQueue.remove( 0 );

            if (!model.hasLoadedImport( importURI )  &&  !ignoringImport( importURI )) {
                // this file has not been processed yet
                loadImport( model, importURI, readQueue );
            }
        }

        // ensure that the reasoner gets to see the updated axioms
        model.rebind();
    }


    /**
     * <p>Unload all of the imports in the queue</p>
     * @param model The model to unload the imports from
     * @param unloadQueue The queue of imports to unload
     */
    protected void unloadImports( OntModel model, List<String> unloadQueue ) {
        while (!unloadQueue.isEmpty()) {
            // we process the import statements as a FIFO queue
            String importURI = unloadQueue.remove( 0 );

            if (model.hasLoadedImport( importURI )) {
                    // any imports that remain are scheduled for removal
                List<String> imports = unload(importURI, model);
                unloadQueue.addAll( imports );
            }
        }

        model.rebind();
    }

    private List<String> unload(String importURI, OntModel model) {
        // this import has not been unloaded yet

        // look up the cached model - if we can't find it, we can't unload the import
        Model importModel = getModel( importURI );
        if ( importModel == null)
            return List.of();

        List<String> imports = new ArrayList<>();

        // collect a list of the imports from the model that is scheduled for removal
        for (StmtIterator i = importModel.listStatements( null, model.getProfile().IMPORTS(), (RDFNode) null ); i.hasNext(); ) {
            imports.add( i.nextStatement().getResource().getURI() );
        }

        // now remove the sub-model
        model.removeSubModel( importModel, false );
        model.removeLoadedImport( importURI );

        // check the list of imports of the model we have removed - if they are not
        // imported by other imports that remain, we should remove them as well
        for (StmtIterator i = model.listStatements( null, model.getProfile().IMPORTS(), (RDFNode) null ); i.hasNext(); ) {
            imports.remove( i.nextStatement().getResource().getURI() );
        }
        return imports;
    }

    /**
     * <p>Add the ontologies imported by the given model to the end of the queue.</p>
     */
    protected void queueImports( Model model, List<String> readQueue, Profile profile ) {
        if (model instanceof OntModel) {
            // add the imported ontologies to the queue
            readQueue.addAll( ((OntModel) model).listImportedOntologyURIs() );
        }
        else {
            // we have to do the query manually
            StmtIterator i = model.listStatements( null, profile.IMPORTS(), (RDFNode) null );

            while (i.hasNext()) {
                // read the next import statement and add to the queue
                readQueue.add( i.nextStatement().getResource().getURI() );
            }
        }
    }


    /**
     * <p>
     * Initialise the mappings for uri's and prefixes by loading metadata
     * from an RDF model.
     * </p>
     *
     * @param path The URI path to search for a loadable model
     */
    protected void initialiseMetadata( String path ) {
        // search the path for metadata about locally cached models
        Model metadata = findMetadata( path );

        if (metadata != null) {
            processMetadata( metadata );
        }
    }


    /**
     * <p>
     * Search the given path for a resolvable URI, from which we load a model
     * (assuming RDF/XML).
     * </p>
     *
     * @param configPath The path to search
     * @return A model loaded by resolving an entry on the path, or null if
     * no path entries succeed.
     */
    protected Model findMetadata( String configPath ) {
        if (configPath == null) {
            return null;
        }

        // Make a temporary file manager to look for the metadata file
        FileManager fm = FileManager.create();
        fm.addLocatorFile();
        fm.addLocatorURL();
        fm.addLocatorClassLoader( fm.getClass().getClassLoader() );

        try {
            String uri = null ;
            InputStream in = null ;

            StringTokenizer pathElems = new StringTokenizer( configPath, FileManager.PATH_DELIMITER );
            while (in == null && pathElems.hasMoreTokens()) {
                uri = pathElems.nextToken();
                in = fm.openNoMap( uri );
            }

            if (in != null) {
                String syntax = FileUtils.guessLang(uri);
                Model model = ModelFactory.createDefaultModel() ;
                model.read( in, uri, syntax );
                m_policyURL = uri;
                return model;
            }
        }
        catch (JenaException e) {
            log.warn( "Exception while reading configuration: " + e.getMessage(), e);
        }

        return null;
    }


    /**
     * <p>
     * Load the ontology specification metadata from the model into the local
     * mapping tables.
     * </p>
     *
     * @param metadata A model containing metadata about ontologies.
     */
    protected void processMetadata( Model metadata ) {
        // there may be configuration for the location mapper in the ODM metadata file
        getFileManager().getLocationMapper().processConfig( metadata );

        // first we process the general policy statements for this document manager
        for (ResIterator i = metadata.listResourcesWithProperty( RDF.type, DOC_MGR_POLICY ); i.hasNext(); ) {
            Resource policy = i.nextResource();

            // iterate over each policy statement
            for (StmtIterator j = policy.listProperties();  j.hasNext(); ) {
                Statement s = j.nextStatement();
                Property pred = s.getPredicate();

                if (pred.equals( CACHE_MODELS )) {
                    setCacheModels( s.getBoolean() );
                }
                else if (pred.equals( PROCESS_IMPORTS )) {
                    setProcessImports( s.getBoolean() );
                }
                else if (pred.equals( IGNORE_IMPORT )) {
                    addIgnoreImport( s.getResource().getURI() );
                }
            }
        }

        // then we look up individual meta-data for particular ontologies
        for (ResIterator i = metadata.listResourcesWithProperty( RDF.type, ONTOLOGY_SPEC ); i.hasNext(); ) {
            Resource root = i.nextResource();

            Statement s = root.getProperty( PUBLIC_URI );
            if (s != null) {
                // this will be the key in the mappings
                String publicURI = s.getResource().getURI();

                // there may be a cached copy for this ontology
                s = root.getProperty( ALT_URL );
                if (s != null) addAltEntry( publicURI, s.getResource().getURI() );
            }
            else {
                log.warn( "Ontology specification node lists no public URI - node ignored");
            }
        }
    }


    /**
     * <p>
     * Load the document referenced by the given URI into the model.  The cache will be
     * used if permitted by the policy, and the imports of loaded model will be added to
     * the end of the queue.
     * </p>
     *
     * @param model The composite model to load into
     * @param importURI The URI of the document to load
     * @param readQueue Cumulative read queue for this operation
     */
    protected void loadImport( OntModel model, String importURI, List<String> readQueue ) {
        if (m_processImports) {
            // add this model to occurs check list
            model.addLoadedImport( importURI );

            Model in = fetchPossiblyCachedImportModel( model, importURI );

            // we trap the case of importing ourself (which may happen via an indirect imports chain)
            if (in != model) {
                // queue the imports from the input model on the end of the read queue
                queueImports( in, readQueue, model.getProfile() );

                // add to the imports union graph, but don't do the rebind yet
                model.addSubModel( in, false );

                // we also cache the model if we haven't seen it before (and caching is on)
                addModel( importURI, in );
            }
        }
    }

    /**
      if we have a cached version get that, otherwise load from the URI but don't do the imports closure
     * @param model
     * @param importURI
     * @return Model
     */
    private Model fetchPossiblyCachedImportModel( OntModel model, String importURI ) {
        Model in = getModel( importURI );

        // if not cached, we must load it from source
        if (in == null) {
            in = fetchLoadedImportModel( model.getSpecification(), importURI );
        }
        return in;
        }


    /**
     * @param spec
     * @param importURI
     * @return Model
     */
    private Model fetchLoadedImportModel( OntModelSpec spec, String importURI ) {
        // workaround - default model maker can apparently create models that are closed
        // TODO: this really suggests a bug in ModelMaker, kers to investigate
        ModelMaker maker = spec.getImportModelMaker();
        if (maker.hasModel( importURI )) {
            Model m = maker.getModel( importURI );
            if (!m.isClosed()) {
                return m;
            }
            else {
                // we don't want to hang on to closed models
                maker.removeModel( importURI );
            }
        }

        // otherwise, we use the model maker to get the model anew
        Model m = spec.getImportModelGetter()
                   .getModel( importURI, new ModelReader() {
                                            @Override
                                            public Model readModel( Model toRead, String URL ) {
                                               read( toRead, URL, true );
                                               return toRead;
                                            }
                                         } );

        return m;
    }


    /**
     * <p>
     * Load into the given model from the given URI, or from a local cache URI if defined.
     * </p>
     *
     * @param model A model to read statements into
     * @param uri A URI string
     * @param warn If true, warn on RDF exception
     * @return True if the uri was read successfully
     */
    protected boolean read( Model model, String uri, boolean warn ) {
        boolean success = false;
        try {
            // invoke the pre-read hook
            String source = m_readHook.beforeRead( model, uri, this );
            if (source == null) {
                log.warn( "Read hook returned null source, so assuming old value: " + uri );
                source = uri;
            }
            else {
                // do the actual read
                getFileManager().readModelInternal( model, source );
            }

            // now the post-read hook
            m_readHook.afterRead( model, source, this );
            success = true;
        }
        catch (Exception e) {
            // if there is a read failure handler, invoke it now
            if (getReadFailureHandler() != null) {
                getReadFailureHandler().handleFailedRead( uri, model, e );
            }
            else {
                // otherwise, log the error
                log.warn( "An error occurred while attempting to read from " + uri + ". Msg was '" + e.getMessage() + "'.", e );
            }
        }
        return success;
    }


    /**
     * <p>Set the default option settings.</p>
     */
    protected void setDefaults() {
        setCacheModels( true );
        setProcessImports( true );
        setDefaultPrefixMappings();
    }


    /**
     * Set the default prefix mappings.
     */
    protected void setDefaultPrefixMappings() {
        m_prefixMap.setNsPrefixes( PrefixMapping.Standard );

        // PrefixMapping.Standard includes dc:, which OntModels traditionally haven't included
        m_prefixMap.removeNsPrefix( "dc" );
    }


    //==============================================================================
    // Inner class definitions
    //==============================================================================

    /**
     * Interface defining a handler call-back in the case that the {@link OntDocumentManager}
     * fails in an attempt to read the contents of a URL into a model.
     */
    public static interface ReadFailureHandler
    {
        /**
         * Behaviour to invoke when the {@link OntDocumentManager} tries and fails
         * to read an ontology document from a given URL.
         * @param url The URL that the OntDocumentManager was trying to read
         * @param model The model that the OntDocumentManager is reading into
         * @param e An exception indicating the reason for the failure to read the document
         */
        public void handleFailedRead( String url, Model model, Exception e );
    }

    /**
     * Interface denoting a handler class that can intervene in the process of
     * reading a source document into a model.
     */
    public static interface ReadHook
    {
        /**
         * <p>Behaviour that is invoked <strong>before</strong> the contents of the
         * given source (URI or filename) are read into the given model. The return
         * value from this method denotes a revised string to use in place of the
         * supplied source string. Handlers are permitted to make state changes
         * to the model and the ODM, but carefully!</p>
         *
         * @param model The model that is going to receive the contents of the source
         * @param source The identity of the source, as a file name or URI
         * @param odm The Ont Document Manager invoking this handler
         * @return The revised name of the source (or the same string if no
         * change to the source is required). Note that if this method returns
         * <code>null</code>, the source <strong>will not be subsequently read.</strong>
         */
        public String beforeRead( Model model, String source, OntDocumentManager odm );

        /**
         * <p>Behaviour that is invoked <strong>just after</strong> the contents of the
         * given source (URI or filename) have been read into the given model.
         * Handlers are permitted to make state changes
         * to the model and the ODM, but carefully!</p>
         *
         * @param model The model that is going to receive the contents of the source
         * @param source The identity of the source, as a file name or URI
         * @param odm The Ont Document Manager invoking this handler
         */
        public void afterRead( Model model, String source, OntDocumentManager odm );
    }

    /**
     * The default implementation of {@link OntDocumentManager.ReadHook} makes no changes.
     */
    public static class DefaultReadHook
        implements ReadHook
    {
        @Override
        public void afterRead( Model model, String source, OntDocumentManager odm ) {
            // do nothing
        }

        @Override
        public String beforeRead( Model model, String source, OntDocumentManager odm ) {
            return source;
        }

    }
}
