/*
 * 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 org.apache.log4j;

import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.or.ObjectRenderer;
import org.apache.log4j.or.RendererMap;
import org.apache.log4j.plugins.Plugin;
import org.apache.log4j.plugins.PluginRegistry;
import org.apache.log4j.scheduler.Scheduler;
import org.apache.log4j.spi.ErrorItem;
import org.apache.log4j.spi.HierarchyEventListener;
import org.apache.log4j.spi.LoggerEventListener;
import org.apache.log4j.spi.LoggerFactory;
import org.apache.log4j.spi.LoggerRepository;
import org.apache.log4j.spi.LoggerRepositoryEventListener;
import org.apache.log4j.spi.LoggerRepositoryEx;
import org.apache.log4j.spi.RendererSupport;
import org.apache.log4j.xml.UnrecognizedElementHandler;
import org.apache.log4j.xml.DOMConfigurator;
import org.w3c.dom.Element;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Vector;


/**
 * This class implements LoggerRepositoryEx by
 *   wrapping an existing LoggerRepository implementation
 *   and implementing the newly added capabilities.
*/
public final class LoggerRepositoryExImpl
        implements LoggerRepositoryEx,
        RendererSupport,
        UnrecognizedElementHandler {

    /**
     * Wrapped logger repository.
     */
  private final LoggerRepository repo;

    /**
     * Logger factory.  Does not affect class of logger
     * created by underlying repository.
     */
  private LoggerFactory loggerFactory;

    /**
     *  Renderer support.
     */
  private final RendererSupport rendererSupport;

    /**
     * List of repository event listeners.
     */
  private final ArrayList repositoryEventListeners = new ArrayList();
    /**
     * Map of HierarchyEventListener keyed by LoggingEventListener.
     */
  private final Map loggerEventListeners = new HashMap();
    /**
     * Name of hierarchy.
     */
  private String name;
    /**
     * Plug in registry.
     */
  private PluginRegistry pluginRegistry;
    /**
     * Properties.
     */
  private final Map properties = new Hashtable();
    /**
     * Scheduler.
     */
  private Scheduler scheduler;

  /** The repository can also be used as an object store
   * for various objects used by log4j components.
   */
  private Map objectMap = new HashMap();


    /**
     * Error list.
     */
  private List errorList = new Vector();

    /**
     * True if hierarchy has not been modified.
     */
  private boolean pristine = true;

  /**
     Constructs a new logger hierarchy.

     @param repository Base implementation of repository.

   */
  public LoggerRepositoryExImpl(final LoggerRepository repository) {
    super();
    if (repository == null) {
        throw new NullPointerException("repository");
    }
    repo = repository;
    if (repository instanceof RendererSupport) {
        rendererSupport = (RendererSupport) repository;
    } else {
        rendererSupport = new RendererSupportImpl();
    }
  }


  /**
    Add a {@link LoggerRepositoryEventListener} to the repository. The
    listener will be called when repository events occur.
    @param listener listener
    */
  public void addLoggerRepositoryEventListener(
    final LoggerRepositoryEventListener listener) {
    synchronized (repositoryEventListeners) {
      if (repositoryEventListeners.contains(listener)) {
        LogLog.warn(
          "Ignoring attempt to add a previously "
                  + "registered LoggerRepositoryEventListener.");
      } else {
        repositoryEventListeners.add(listener);
      }
    }
  }


  /**
    Remove a {@link LoggerRepositoryEventListener} from the repository.
    @param listener listener
    */
  public void removeLoggerRepositoryEventListener(
    final LoggerRepositoryEventListener listener) {
    synchronized (repositoryEventListeners) {
      if (!repositoryEventListeners.contains(listener)) {
        LogLog.warn(
          "Ignoring attempt to remove a "
                  + "non-registered LoggerRepositoryEventListener.");
      } else {
        repositoryEventListeners.remove(listener);
      }
    }
  }

  /**
    Add a {@link LoggerEventListener} to the repository. The  listener
    will be called when repository events occur.
    @param listener listener
   */
  public void addLoggerEventListener(final LoggerEventListener listener) {
    synchronized (loggerEventListeners) {
      if (loggerEventListeners.get(listener) != null) {
        LogLog.warn(
         "Ignoring attempt to add a previously registerd LoggerEventListener.");
      } else {
        HierarchyEventListenerProxy proxy =
                new HierarchyEventListenerProxy(listener);
        loggerEventListeners.put(listener, proxy);
        repo.addHierarchyEventListener(proxy);
      }
    }
  }

    /**
       Add a {@link org.apache.log4j.spi.HierarchyEventListener}
     event to the repository.
     @param listener listener
       @deprecated Superceded by addLoggerEventListener
    */
    public
    void addHierarchyEventListener(final HierarchyEventListener listener) {
        repo.addHierarchyEventListener(listener);
    }


  /**
    Remove a {@link LoggerEventListener} from the repository.
    @param listener listener to be removed
    */
  public void removeLoggerEventListener(final LoggerEventListener listener) {
    synchronized (loggerEventListeners) {
      HierarchyEventListenerProxy proxy =
              (HierarchyEventListenerProxy) loggerEventListeners.get(listener);
      if (proxy == null) {
        LogLog.warn(
          "Ignoring attempt to remove a non-registered LoggerEventListener.");
      } else {
        loggerEventListeners.remove(listener);
        proxy.disable();
      }
    }
  }

    /**
     * Issue warning that there are no appenders in hierarchy.
     * @param cat logger, not currently used.
     */
  public void emitNoAppenderWarning(final Category cat) {
    repo.emitNoAppenderWarning(cat);
  }

  /**
     Check if the named logger exists in the hierarchy. If so return
     its reference, otherwise returns <code>null</code>.

     @param loggerName The name of the logger to search for.
     @return true if logger exists.
  */
  public Logger exists(final String loggerName) {
    return repo.exists(loggerName);
  }

  /**
   * Return the name of this hierarchy.
   * @return name of hierarchy
   */
  public String getName() {
    return name;
  }

  /**
   * Set the name of this repository.
   *
   * Note that once named, a repository cannot be rerenamed.
   * @param repoName name of hierarchy
   */
  public void setName(final String repoName) {
    if (name == null) {
      name = repoName;
    } else if (!name.equals(repoName)) {
      throw new IllegalStateException(
        "Repository [" + name + "] cannot be renamed as [" + repoName + "].");
    }
  }

  /**
   * {@inheritDoc}
   */
  public Map getProperties() {
    return properties;
  }

  /**
   * {@inheritDoc}
   */
  public String getProperty(final String key) {
     return (String) properties.get(key);
  }

  /**
   * Set a property by key and value. The property will be shared by all
   * events in this repository.
   * @param key property name
   * @param value property value
   */
  public void setProperty(final String key,
                          final String value) {
   properties.put(key, value);
  }

  /**
     The string form of {@link #setThreshold(Level)}.
   @param levelStr symbolic name for level
  */
  public void setThreshold(final String levelStr) {
    repo.setThreshold(levelStr);
  }

  /**
     Enable logging for logging requests with level <code>l</code> or
     higher. By default all levels are enabled.

     @param l The minimum level for which logging requests are sent to
     their appenders.  */
  public void setThreshold(final Level l) {
    repo.setThreshold(l);
  }

  /**
   * {@inheritDoc}
   */
  public PluginRegistry getPluginRegistry() {
   if (pluginRegistry == null) {
     pluginRegistry = new PluginRegistry(this);
   }
   return pluginRegistry;
  }


    /**
      Requests that a appender added event be sent to any registered
      {@link LoggerEventListener}.
      @param logger The logger to which the appender was added.
      @param appender The appender added to the logger.
     */
    public void fireAddAppenderEvent(final Category logger,
                                     final Appender appender) {
        repo.fireAddAppenderEvent(logger, appender);
    }


    /**
      Requests that a appender removed event be sent to any registered
      {@link LoggerEventListener}.
      @param logger The logger from which the appender was removed.
      @param appender The appender removed from the logger.
      */
    public void fireRemoveAppenderEvent(final Category logger,
                                        final Appender appender) {
       if (repo instanceof Hierarchy) {
           ((Hierarchy) repo).fireRemoveAppenderEvent(logger, appender);
       }
    }


  /**
    Requests that a level changed event be sent to any registered
    {@link LoggerEventListener}.
    @param logger The logger which changed levels.
    */
  public void fireLevelChangedEvent(final Logger logger) {
  }

  /**
   *
   * Requests that a configuration changed event be sent to any registered
   * {@link LoggerRepositoryEventListener}.
   * 
   */
  public void fireConfigurationChangedEvent() {
  }


  /**
     Returns the current threshold.
     @return current threshold level

     @since 1.2 */
  public Level getThreshold() {
    return repo.getThreshold();
  }


  /**
     Return a new logger instance named as the first parameter using
     the default factory.

     <p>If a logger of that name already exists, then it will be
     returned.  Otherwise, a new logger will be instantiated and
     then linked with its existing ancestors as well as children.

     @param loggerName The name of the logger to retrieve.
     @return logger

  */
  public Logger getLogger(final String loggerName) {
    return repo.getLogger(loggerName);
  }

  /**
      Return a new logger instance named as the first parameter using
      <code>factory</code>.

      <p>If a logger of that name already exists, then it will be
      returned.  Otherwise, a new logger will be instantiated by the
      <code>factory</code> parameter and linked with its existing
      ancestors as well as children.

      @param loggerName The name of the logger to retrieve.
      @param factory The factory that will make the new logger instance.
      @return logger

  */
  public Logger getLogger(final String loggerName,
                          final LoggerFactory factory) {
    return repo.getLogger(loggerName, factory);
  }

  /**
     Returns all the currently defined categories in this hierarchy as
     an {@link java.util.Enumeration Enumeration}.

     <p>The root logger is <em>not</em> included in the returned
     {@link Enumeration}.
     @return enumerator of current loggers
   */
  public Enumeration getCurrentLoggers() {
    return repo.getCurrentLoggers();
  }

  /**
   * Return the the list of previously encoutered {@link ErrorItem error items}.
   * @return list of errors
   */
  public List getErrorList() {
    return errorList;
  }

  /**
   * Add an error item to the list of previously encountered errors.
   * @param errorItem error to add to list of errors.
   */
  public void addErrorItem(final ErrorItem errorItem) {
    getErrorList().add(errorItem);
  }

  /**
   * Get enumerator over current loggers.
   * @return enumerator over current loggers
     @deprecated Please use {@link #getCurrentLoggers} instead.
   */
  public Enumeration getCurrentCategories() {
    return repo.getCurrentCategories();
  }

  /**
     Get the renderer map for this hierarchy.
   @return renderer map
  */
  public RendererMap getRendererMap() {
    return rendererSupport.getRendererMap();
  }

  /**
     Get the root of this hierarchy.

     @since 0.9.0
   @return root of hierarchy
   */
  public Logger getRootLogger() {
    return repo.getRootLogger();
  }

  /**
     This method will return <code>true</code> if this repository is
     disabled for <code>level</code> value passed as parameter and
     <code>false</code> otherwise. See also the {@link
     #setThreshold(Level) threshold} method.
   @param level numeric value for level.
   @return true if disabled for specified level
   */
  public boolean isDisabled(final int level) {
    return repo.isDisabled(level);
  }

  /**
     Reset all values contained in this hierarchy instance to their
     default.  This removes all appenders from all categories, sets
     the level of all non-root categories to <code>null</code>,
     sets their additivity flag to <code>true</code> and sets the level
     of the root logger to DEBUG.  Moreover,
     message disabling is set its default "off" value.

     <p>Existing categories are not removed. They are just reset.

     <p>This method should be used sparingly and with care as it will
     block all logging until it is completed.</p>

     @since 0.8.5 */
  public void resetConfiguration() {
    repo.resetConfiguration();
  }

  /**
     Used by subclasses to add a renderer to the hierarchy passed as parameter.
   @param renderedClass class
   @param renderer object used to render class.
   */
  public void setRenderer(final Class renderedClass,
                          final ObjectRenderer renderer) {
    rendererSupport.setRenderer(renderedClass, renderer);
  }

  /**
   * {@inheritDoc}
   */
  public boolean isPristine() {
    return pristine;
  }

  /**
   * {@inheritDoc}
   */
  public void setPristine(final boolean state) {
    pristine = state;
  }

  /**
     Shutting down a hierarchy will <em>safely</em> close and remove
     all appenders in all categories including the root logger.

     <p>Some appenders such as org.apache.log4j.net.SocketAppender
     and AsyncAppender need to be closed before the
     application exists. Otherwise, pending logging events might be
     lost.

     <p>The <code>shutdown</code> method is careful to close nested
     appenders before closing regular appenders. This is allows
     configurations where a regular appender is attached to a logger
     and again to a nested appender.

     @since 1.0 */
  public void shutdown() {
    repo.shutdown();
  }


  /**
   * Return this repository's own scheduler.
   * The scheduler is lazily instantiated.
   * @return this repository's own scheduler.
   */
  public Scheduler getScheduler() {
    if (scheduler == null) {
      scheduler = new Scheduler();
      scheduler.setDaemon(true);
      scheduler.start();
    }
    return scheduler;
  }

    /**
     * Puts object by key.
     * @param key key, may not be null.
     * @param value object to associate with key.
     */
  public void putObject(final String key,
                        final Object value) {
    objectMap.put(key, value);
  }

    /**
     * Get object by key.
     * @param key key, may not be null.
     * @return object associated with key or null.
     */
  public Object getObject(final String key) {
    return objectMap.get(key);
  }

    /**
     * Set logger factory.
     * @param factory logger factory.
     */
  public void setLoggerFactory(final LoggerFactory factory) {
    if (factory == null) {
      throw new NullPointerException();
    }
    this.loggerFactory = factory;
  }

    /**
     * Get logger factory.
     * @return logger factory.
     */
  public LoggerFactory getLoggerFactory() {
    return loggerFactory;
  }

    /** {@inheritDoc} */
    public boolean parseUnrecognizedElement(
            final Element element,
            final Properties props) throws Exception {
        if ("plugin".equals(element.getNodeName())) {
            Object instance =
                    DOMConfigurator.parseElement(element, props, Plugin.class);
            if (instance instanceof Plugin) {
                Plugin plugin = (Plugin) instance;
                String pluginName = DOMConfigurator.subst(element.getAttribute("name"), props);
                if (pluginName.length() > 0) {
                    plugin.setName(pluginName);
                }
                getPluginRegistry().addPlugin(plugin);
                plugin.setLoggerRepository(this);

                LogLog.debug("Pushing plugin on to the object stack.");
                plugin.activateOptions();
                return true;
            }
        }
        return false;
    }



    /**
     * Implementation of RendererSupportImpl if not
     * provided by LoggerRepository.
     */
   private static final class RendererSupportImpl implements RendererSupport {
        /**
         * Renderer map.
         */
       private final RendererMap renderers = new RendererMap();

        /**
         * Create new instance.
         */
       public RendererSupportImpl() {
           super();
       }

        /** {@inheritDoc} */
       public RendererMap getRendererMap() {
           return renderers;
       }

        /** {@inheritDoc} */
       public void setRenderer(final Class renderedClass,
                               final ObjectRenderer renderer) {
           renderers.put(renderedClass, renderer);
       }
   }

    /**
     * Proxy that implements HierarchyEventListener
     * and delegates to LoggerEventListener.
     */
   private static final class HierarchyEventListenerProxy
        implements HierarchyEventListener {
        /**
         * Wrapper listener.
         */
       private LoggerEventListener listener;

        /**
         * Creates new instance.
         * @param l listener
         */
       public HierarchyEventListenerProxy(final LoggerEventListener l) {
           super();
           if (l == null) {
               throw new NullPointerException("l");
           }
           listener = l;
       }

        /** {@inheritDoc} */
       public void addAppenderEvent(final Category cat,
                                    final Appender appender) {
           if (isEnabled() && cat instanceof Logger) {
                listener.appenderAddedEvent((Logger) cat, appender);
           }
       }

        /** {@inheritDoc} */
       public void removeAppenderEvent(final Category cat,
                                    final Appender appender) {
           if (isEnabled() && cat instanceof Logger) {
                listener.appenderRemovedEvent((Logger) cat, appender);
           }
       }

        /**
         * Disable forwarding of notifications to
         * simulate removal of listener.
         */
       public synchronized void disable() {
           listener = null;
       }

        /**
         * Gets whether proxy is enabled.
         * @return true if proxy is enabled.
         */
       private synchronized boolean isEnabled() {
           return listener != null;
       }
   }

}
