/*
 * Copyright 2005 Ralf Joachim
 *
 * Licensed 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.exolab.castor.mapping;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.castor.mapping.MappingSource;
import org.castor.util.Messages;
import org.exolab.castor.mapping.xml.MappingRoot;
import org.exolab.castor.net.util.URIUtils;
import org.exolab.castor.util.DTDResolver;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Utility class for loading mapping files and providing them to the
 * XML marshaller, JDO engine etc. The mapping file can be loaded from
 * a URL, input stream or SAX <tt>InputSource</tt>.
 * <p>
 * Multiple mapping files can be loaded with the same <tt>Mapping</tt>
 * object. When loading master mapping files that include other mapping
 * files it might be convenient to use {@link #setBaseURL} or {@link
 * #setEntityResolver}.
 * <p>
 * If the desired class loader is different than the one used by Castor
 * (e.g. if Castor is installed as a Java extension), the <tt>Mapping</tt>
 * object can be constructed with the proper class loader.
 * <p>
 * The following example loads two mapping files:
 * <pre>
 * Mapping mapping;
 *
 * mapping = new Mapping( getClass().getClassLoader() );
 * mapping.loadMapping( "mapping.xml" );
 * mapping.loadMapping( url );
 * </pre>
 *
 * @author <a href="mailto:arkin AT intalio DOT com">Assaf Arkin</a>
 * @author <a href="mailto:ralf DOT joachim AT syscon-world DOT de">Ralf Joachim</a>
 * @version $Revision: 6312 $ $Date: 2006-04-25 16:09:10 -0600 (Tue, 25 Apr 2006) $
 */
public final class Mapping {
    //--------------------------------------------------------------------------
    
    /** The <a href="http://jakarta.apache.org/commons/logging/">Jakarta Commons
     *  Logging </a> instance used for all logging. */
    private static final Log LOG = LogFactory.getLog(Mapping.class);
    
    private static final String DEFAULT_SOURCE_TYPE = "CastorXmlMapping";
    
    /** List of mapping sources to resolve. */
    private final List _mappings = new ArrayList();

    /** Set of processed systemID's. */
    private final Set _processed = new HashSet();

    /** The loaded mapping. */
    private final MappingRoot _root = new MappingRoot();

    /** The class loader to use. */
    private final ClassLoader _classLoader;
    
    /** The entity resolver to use. May be null. */
    private DTDResolver _resolver = new DTDResolver();
    
    //--------------------------------------------------------------------------

    /**
     * Constructs a new mapping.
     *
     * @param loader The class loader to use, null for the default
     */
    public Mapping(final ClassLoader loader) {
        if (loader == null) {
            _classLoader = getClass().getClassLoader();
        } else {
            _classLoader = loader;
        }
    }

    /**
     * Constructs a new mapping.
     */
    public Mapping() { this(null); }

    //--------------------------------------------------------------------------

    /**
     * Get list of mapping sources to resolve.
     * 
     * @return List of mapping sources to resolve.
     * @throws MappingException If no mapping source has been loaded previously.
     */
    public List getMappingSources() throws MappingException {
        if (_mappings.size() == 0) {
            throw new MappingException("Must call loadMapping first");
        }
        return Collections.unmodifiableList(_mappings);
    }

    /**
     * Marks the given mapping as having been processed.
     * 
     * @param id systemID or stream to identify the mapping to mark.
     */
    public void markAsProcessed(final Object id) {
        _processed.add(id);
    }

    /**
     * Returns true if the given systemID or stream has been marked as processed.
     * 
     * @param id systemID or stream to check for being marked as processed.
     * @return true if the given systemID or stream has been marked as processed.
     */
    public boolean processed(final Object id) {
        return _processed.contains(id);
    }
    
    /**
     * Get the loaded mapping.
     * 
     * @return The loaded mapping.
     */
    public MappingRoot getRoot() { return _root; }
    
    //--------------------------------------------------------------------------

    /**
     * Returns the class loader used by this mapping object. The returned
     * class loaded may be the one passed in the constructor, the one used
     * to load Castor, or in some 1.1 JVMs null.
     *
     * @return The class loader used by this mapping object (may be null)
     */
    public ClassLoader getClassLoader() {
        return _classLoader;
    }
    
    /**
     * Sets the entity resolver. The entity resolver can be used to
     * resolve external entities and cached documents that are used
     * from within mapping files.
     *
     * @param resolver The entity resolver to use
     */
    public void setEntityResolver(final EntityResolver resolver) {
        _resolver = new DTDResolver(resolver);
    }

    /**
     * Sets the base URL for the mapping and related files. If the base
     * URL is known, files can be included using relative names. Any URL
     * can be passed, if the URL can serve as a base URL it will be used.
     * If url is an absolute path, it is converted to a file URL.
     *
     * @param url The base URL
     */
    public void setBaseURL(final String url) {
        String location = url;
        //-- remove filename if necessary:
        if (location != null) {        
            int idx = location.lastIndexOf('/');
            if (idx < 0) idx = location.lastIndexOf('\\');
            if (idx >= 0) {
                int extIdx = location.indexOf('.', idx);
                if (extIdx > 0) {
                    location = location.substring(0, idx);
                }
            }
        }
        
        try {
          _resolver.setBaseURL(new URL(location));
        } catch (MalformedURLException except) {
          // try to parse the url as an absolute path
          try {
              LOG.info(Messages.format("mapping.wrongURL", location));
              _resolver.setBaseURL(new URL("file", null, location));
          } catch (MalformedURLException except2) { }
        }
    }

    //--------------------------------------------------------------------------

    /**
     * Loads the mapping from the specified URL with type defaults to
     * 'CastorXmlMapping'. If an entity resolver was specified, will use
     * the entity resolver to resolve the URL. This method is also used
     * to load mappings referenced from another mapping or configuration
     * file.
     *
     * @param url The URL of the mapping file.
     * @throws IOException An error occured when reading the mapping file.
     * @throws MappingException The mapping file is invalid.
     */
    public void loadMapping(final String url) throws IOException, MappingException {
        loadMapping(url, DEFAULT_SOURCE_TYPE);
    }

    /**
     * Loads the mapping from the specified URL. If an entity resolver
     * was specified, will use the entity resolver to resolve the URL.
     * This method is also used to load mappings referenced from another
     * mapping or configuration file.
     *
     * @param url The URL of the mapping file.
     * @param type The source type.
     * @throws IOException An error occured when reading the mapping file.
     * @throws MappingException The mapping file is invalid.
     */
    public void loadMapping(final String url, final String type)
    throws IOException, MappingException {
        String location = url;
        if (_resolver.getBaseURL() == null) {
            setBaseURL(location);
            location = URIUtils.getRelativeURI(location);
        }
        try {
            InputSource source = _resolver.resolveEntity(null, location);
            if (source == null) { source = new InputSource(location); }
            if (source.getSystemId() == null) { source.setSystemId(location); }
            LOG.info(Messages.format("mapping.loadingFrom", location));
            loadMapping(source, type);
        } catch (SAXException ex) {
            throw new MappingException(ex);
        }
    }

    /**
     * Loads the mapping from the specified URL with type defaults to
     * 'CastorXmlMapping'.
     *
     * @param url The URL of the mapping file.
     * @throws IOException An error occured when reading the mapping file.
     * @throws MappingException The mapping file is invalid.
     */
    public void loadMapping(final URL url) throws IOException, MappingException {
        loadMapping(url, DEFAULT_SOURCE_TYPE);
    }

    /**
     * Loads the mapping from the specified URL.
     *
     * @param url The URL of the mapping file.
     * @param type The source type.
     * @throws IOException An error occured when reading the mapping file.
     * @throws MappingException The mapping file is invalid.
     */
    public void loadMapping(final URL url, final String type)
    throws IOException, MappingException {
        try {
            if (_resolver.getBaseURL() == null) {
                _resolver.setBaseURL(url);
            }
            InputSource source = _resolver.resolveEntity(null, url.toExternalForm());
            if (source == null) {
                source = new InputSource(url.toExternalForm());
                source.setByteStream(url.openStream());
            } else
                source.setSystemId(url.toExternalForm());
            LOG.info(Messages.format("mapping.loadingFrom", url.toExternalForm()));
           loadMapping(source, type);
        } catch (SAXException ex) {
            throw new MappingException(ex);
        }
    }

    /**
     * Loads the mapping from the specified input source with type defaults to
     * 'CastorXmlMapping'.
     *
     * @param source The input source.
     */
    public void loadMapping(final InputSource source) {
        loadMapping(source, DEFAULT_SOURCE_TYPE);
    }

    /**
     * Loads the mapping from the specified input source.
     *
     * @param source The input source.
     * @param type The source type.
     */
    public void loadMapping(final InputSource source, final String type) {
        _mappings.add(new MappingSource(source, type, _resolver));
    }

    //--------------------------------------------------------------------------
}


