/*
 * 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.wicket.jmx;

import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;

import javax.management.Attribute;
import javax.management.AttributeList;
import javax.management.InstanceAlreadyExistsException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import javax.management.StandardMBean;

import org.apache.wicket.IInitializer;
import org.apache.wicket.ThreadContext;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.jmx.wrapper.Application;
import org.apache.wicket.jmx.wrapper.ApplicationSettings;
import org.apache.wicket.jmx.wrapper.DebugSettings;
import org.apache.wicket.jmx.wrapper.MarkupSettings;
import org.apache.wicket.jmx.wrapper.PageSettings;
import org.apache.wicket.jmx.wrapper.RequestCycleSettings;
import org.apache.wicket.jmx.wrapper.RequestLogger;
import org.apache.wicket.jmx.wrapper.ResourceSettings;
import org.apache.wicket.jmx.wrapper.SecuritySettings;
import org.apache.wicket.jmx.wrapper.SessionSettings;
import org.apache.wicket.jmx.wrapper.StoreSettings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Registers Wicket's MBeans.
 * <p>
 * Users can specify the MBeanServer implementation in which to register the MBeans by setting the
 * <code>org.apache.wicket.mbean.server.agentid</code> property to the agent id of the MBeanServer
 * implementation they want, or by setting <code>org.apache.wicket.mbean.server.class</code> to the
 * mbean server class they want (if both are provided, and the agent id returns a server, that one
 * is used). This initializer will log an error when no mbean server with the provided agent id can
 * be found, and will then fall back to use the platform mbean server. When no agent id is provided,
 * the platform mbean server will be used.
 * 
 * @author eelcohillenius
 * @author David Hosier
 */
public class Initializer implements IInitializer
{
	private static final Logger log = LoggerFactory.getLogger(Initializer.class);

	// It's best to store a reference to the MBeanServer rather than getting it
	// over and over
	private MBeanServer mbeanServer = null;

	/**
	 * List of registered names
	 */
	private final List<ObjectName> registered = new ArrayList<>();

	@Override
	public void destroy(final org.apache.wicket.Application application)
	{
		for (ObjectName objectName : registered)
		{
			try
			{
				mbeanServer.unregisterMBean(objectName);
			}
			catch (InstanceNotFoundException | MBeanRegistrationException e)
			{
				log.error(e.getMessage(), e);
			}
		}
	}

	@Override
	public void init(final org.apache.wicket.Application application)
	{
		try
		{
			String name = application.getName();

			String agentId = null;
			try
			{
				agentId = System.getProperty("wicket.mbean.server.agentid");
			}
			catch (SecurityException e)
			{
				// Ignore - we're not allowed to read this property.
				log.warn("not allowed to read property wicket.mbean.server.agentid due to security settings; ignoring");
			}
			if (agentId != null)
			{
				ArrayList<MBeanServer> mbeanServers = MBeanServerFactory.findMBeanServer(agentId);
				if (!mbeanServers.isEmpty())
				{
					mbeanServer = mbeanServers.get(0); // get first
				}
				else
				{
					log.error("unable to find mbean server with agent id {}", agentId);
				}
			}
			if (mbeanServer == null)
			{
				String impl = null;
				try
				{
					impl = System.getProperty("wicket.mbean.server.class");
				}
				catch (SecurityException e)
				{
					// Ignore - we're not allowed to read this property.
					log.warn("Not allowed to read property wicket.mbean.server.class due to security settings: {}. Ignoring",
							e.getMessage());
				}
				if (impl != null)
				{
					ArrayList<MBeanServer> mbeanServers = MBeanServerFactory.findMBeanServer(null);
					if (!mbeanServers.isEmpty())
					{
						for (MBeanServer mbs : mbeanServers)
						{
							if (mbs.getClass().getName().equals(impl))
							{
								mbeanServer = mbs;
								break;
							}
						}
					}
					if (mbeanServer == null)
					{
						log.error("unable to find mbean server of type '{}'", impl);
					}
				}
			}
			if (mbeanServer == null)
			{
				mbeanServer = ManagementFactory.getPlatformMBeanServer();
				// never null
			}

			log.info("registering Wicket mbeans with server '{}'", mbeanServer);

			// register top level application object, but first check whether
			// multiple instances of the same application (name) are running and
			// if so adjust the name
			String domain = "org.apache.wicket.app." + name;
			ObjectName appBeanName = new ObjectName(domain + ":type=Application");
			String tempDomain = domain;
			int i = 0;
			while (mbeanServer.isRegistered(appBeanName))
			{
				tempDomain = name + "-" + i++;
				appBeanName = new ObjectName(tempDomain + ":type=Application");
			}
			domain = tempDomain;

			Application appBean = new Application(application);
			register(application, appBean, appBeanName);

			register(application, new ApplicationSettings(application), new ObjectName(domain
				+ ":type=Application,name=ApplicationSettings"));
			register(application, new DebugSettings(application), new ObjectName(domain
				+ ":type=Application,name=DebugSettings"));
			register(application, new MarkupSettings(application), new ObjectName(domain
				+ ":type=Application,name=MarkupSettings"));
			register(application, new ResourceSettings(application), new ObjectName(domain
				+ ":type=Application,name=ResourceSettings"));
			register(application, new PageSettings(application), new ObjectName(domain
				+ ":type=Application,name=PageSettings"));
			register(application, new RequestCycleSettings(application), new ObjectName(domain
				+ ":type=Application,name=RequestCycleSettings"));
			register(application, new SecuritySettings(application), new ObjectName(domain
				+ ":type=Application,name=SecuritySettings"));
			register(application, new SessionSettings(application), new ObjectName(domain
				+ ":type=Application,name=SessionSettings"));
			register(application, new StoreSettings(application), new ObjectName(domain
				+ ":type=Application,name=StoreSettings"));

			RequestLogger sessionsBean = new RequestLogger(application);
			ObjectName sessionsBeanName = new ObjectName(domain + ":type=RequestLogger");
			register(application, sessionsBean, sessionsBeanName);
		}
		catch (MalformedObjectNameException | InstanceAlreadyExistsException |
				MBeanRegistrationException | NotCompliantMBeanException e)
		{
			throw new WicketRuntimeException(e);
		}
	}

	@Override
	public String toString()
	{
		return "Wicket JMX initializer";
	}

	/**
	 * Register MBean.
	 * 
	 * @param o
	 *            MBean
	 * @param objectName
	 *            Object name
	 * 
	 * @throws NotCompliantMBeanException
	 * @throws MBeanRegistrationException
	 * @throws InstanceAlreadyExistsException
	 */
	private <T> void register(final org.apache.wicket.Application application, final T o,
		final ObjectName objectName) throws InstanceAlreadyExistsException,
		MBeanRegistrationException, NotCompliantMBeanException
	{
		StandardMBean bean = new StandardMBean(o, (Class<T>)o.getClass().getInterfaces()[0]) {
			@Override
			public Object getAttribute(String attribute)
			{
				return withApplication(() -> super.getAttribute(attribute));
			}
			
			@Override
			public void setAttribute(Attribute attribute)
			{
				withApplication(() -> {
					super.setAttribute(attribute);
					return null;
				});
			}
			
			@Override
			public AttributeList setAttributes(AttributeList attributes)
			{
				return withApplication(() -> super.setAttributes(attributes));
			}
			
			@Override
			public Object invoke(String actionName, Object[] params, String[] signature)
			{
				return withApplication(() -> super.invoke(actionName, params, signature));
			}
			
			private <R> R withApplication(Callable<R> callable)
			{
				boolean existed = ThreadContext.exists();

				if (existed == false)
				{
					ThreadContext.setApplication(application);
				}

				try
				{
					return callable.call();
				}
				catch (Exception ex)
				{
					throw new WicketRuntimeException(ex);
				}
				finally
				{
					if (existed == false)
					{
						ThreadContext.detach();
					}
				}
			}
		};
		mbeanServer.registerMBean(bean, objectName);
		registered.add(objectName);
	}
}
