/*
 * 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.core.util.Messages;
import org.castor.mapping.MappingSource;
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 DOT eu">Ralf Joachim</a>
 * @version $Revision$ $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<MappingSource> _mappings = new ArrayList<>();

  /** Set of processed systemID's. */
  private final Set<Object> _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<MappingSource> getMappingSources() throws MappingException {
    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));
  }

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


