/**
 * 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.activemq.broker.virtual;

import java.io.IOException;
import java.net.URI;

import javax.jms.Connection;
import javax.jms.Session;
import javax.jms.Topic;
import javax.jms.TopicSubscriber;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanServerConnection;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

import org.apache.activemq.EmbeddedBrokerTestSupport;

import org.apache.activemq.broker.Broker;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.ConnectionContext;
import org.apache.activemq.broker.ProducerBrokerExchange;
import org.apache.activemq.broker.region.Destination;
import org.apache.activemq.broker.region.DestinationFilter;
import org.apache.activemq.broker.region.DestinationInterceptor;
import org.apache.activemq.command.ActiveMQDestination;
import org.apache.activemq.command.Message;
import org.apache.activemq.xbean.XBeanBrokerFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Test for AMQ-4571.
 * checks that durable subscription is fully unregistered
 * when using nested destination interceptors.
 */
public class DestinationInterceptorDurableSubTest extends EmbeddedBrokerTestSupport {

   private static final transient Logger LOG = LoggerFactory.getLogger(DestinationInterceptorDurableSubTest.class);
   private MBeanServerConnection mbsc = null;
   public static final String JMX_CONTEXT_BASE_NAME = "org.apache.activemq:type=Broker,brokerName=localhost,destinationType=Topic,destinationName=";

   /**
    * Tests AMQ-4571.
    *
    * @throws Exception
    */
   public void testVirtualTopicRemoval() throws Exception {

      LOG.debug("Running testVirtualTopicRemoval()");
      String clientId1 = "myId1";
      String clientId2 = "myId2";

      Connection conn = null;
      Session session = null;

      try {
         assertTrue(broker.isStarted());

         // create durable sub 1
         conn = createConnection();
         conn.setClientID(clientId1);
         conn.start();
         session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
         // Topic topic = session.createTopic(destination.getPhysicalName());
         TopicSubscriber sub1 = session.createDurableSubscriber((Topic) destination, clientId1);

         // create durable sub 2
         TopicSubscriber sub2 = session.createDurableSubscriber((Topic) destination, clientId2);

         // verify two subs registered in JMX
         assertSubscriptionCount(destination.getPhysicalName(), 2);
         assertTrue(isSubRegisteredInJmx(destination.getPhysicalName(), clientId1));
         assertTrue(isSubRegisteredInJmx(destination.getPhysicalName(), clientId2));

         // delete sub 1
         sub1.close();
         session.unsubscribe(clientId1);

         // verify only one sub registered in JMX
         assertSubscriptionCount(destination.getPhysicalName(), 1);
         assertFalse(isSubRegisteredInJmx(destination.getPhysicalName(), clientId1));
         assertTrue(isSubRegisteredInJmx(destination.getPhysicalName(), clientId2));

         // delete sub 2
         sub2.close();
         session.unsubscribe(clientId2);

         // verify no sub registered in JMX
         assertSubscriptionCount(destination.getPhysicalName(), 0);
         assertFalse(isSubRegisteredInJmx(destination.getPhysicalName(), clientId1));
         assertFalse(isSubRegisteredInJmx(destination.getPhysicalName(), clientId2));
      }
      finally {
         session.close();
         conn.close();
      }
   }

   /**
    * Connects to broker using JMX
    *
    * @return The JMX connection
    * @throws IOException in case of any errors
    */
   protected MBeanServerConnection connectJMXBroker() throws IOException {
      // connect to broker via JMX
      JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://:1299/jmxrmi");
      JMXConnector jmxc = JMXConnectorFactory.connect(url, null);
      MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();
      LOG.debug("JMX connection established");
      return mbsc;
   }

   /**
    * Asserts that the Subscriptions JMX attribute of a topic has the expected
    * count.
    *
    * @param topicName     name of the topic destination
    * @param expectedCount expected number of subscriptions
    * @return
    */
   protected boolean assertSubscriptionCount(String topicName, int expectedCount) {
      try {
         if (mbsc == null) {
            mbsc = connectJMXBroker();
         }
         // query broker queue size
         ObjectName[] tmp = (ObjectName[]) mbsc.getAttribute(new ObjectName(JMX_CONTEXT_BASE_NAME + topicName), "Subscriptions");
         assertEquals(expectedCount, tmp.length);
      }
      catch (Exception ex) {
         LOG.error(ex.getMessage());
         return false;
      }
      return true;
   }

   /**
    * Checks if a subscriptions for topic topicName with subName is registered in JMX
    *
    * @param topicName physical name of topic destination (excluding prefix 'topic://')
    * @param subName   name of the durable subscription
    * @return true if registered, false otherwise
    */
   protected boolean isSubRegisteredInJmx(String topicName, String subName) {

      try {
         if (mbsc == null) {
            mbsc = connectJMXBroker();
         }

         // A durable sub is registered under the Subscriptions JMX attribute of the topic and
         // as its own ObjectInstance under the topic's Consumer namespace.
         // AMQ-4571 only removed the latter not the former on unsubscribe(), so we need
         // to check against both.
         ObjectName[] names = (ObjectName[]) mbsc.getAttribute(new ObjectName(JMX_CONTEXT_BASE_NAME + topicName), "Subscriptions");
         ObjectInstance instance = (ObjectInstance) mbsc.getObjectInstance(new ObjectName(JMX_CONTEXT_BASE_NAME +
                                                                                             topicName +
                                                                                             ",endpoint=Consumer,clientId=myId1,consumerId=Durable(myId1_" +
                                                                                             subName +
                                                                                             ")"));

         if (instance == null)
            return false;

         for (int i = 0; i < names.length; i++) {
            if (names[i].toString().contains(subName))
               return true;
         }
      }
      catch (InstanceNotFoundException ine) {
         //this may be expected so log at info level
         LOG.info(ine.toString());
         return false;
      }
      catch (Exception ex) {
         LOG.error(ex.toString());
         return false;
      }
      return false;
   }

   protected void tearDown() throws Exception {
      super.tearDown();
   }

   protected BrokerService createBroker() throws Exception {
      XBeanBrokerFactory factory = new XBeanBrokerFactory();
      BrokerService answer = factory.createBroker(new URI(getBrokerConfigUri()));

      // lets disable persistence as we are a test
      answer.setPersistent(false);
      useTopic = true;
      return answer;
   }

   protected String getBrokerConfigUri() {
      return "org/apache/activemq/broker/virtual/virtual-topics-and-interceptor.xml";
   }

   /**
    * Simple but custom topic interceptor.
    * To be used for testing nested interceptors in conjunction with
    * virtual topic interceptor.
    */
   public static class SimpleDestinationInterceptor implements DestinationInterceptor {

      private final Logger LOG = LoggerFactory.getLogger(SimpleDestinationInterceptor.class);
      private BrokerService broker;

      public SimpleDestinationInterceptor() {
      }

      /* (non-Javadoc)
       * @see org.apache.activemq.broker.BrokerServiceAware#setBrokerService(org.apache.activemq.broker.BrokerService)
       */
      public void setBrokerService(BrokerService brokerService) {
         LOG.info("setBrokerService()");
         this.broker = brokerService;
      }

      /* (non-Javadoc)
       * @see org.apache.activemq.broker.region.DestinationInterceptor#intercept(org.apache.activemq.broker.region.Destination)
       */
      public Destination intercept(final Destination destination) {
         LOG.info("intercept({})", destination.getName());

         if (!destination.getActiveMQDestination().getPhysicalName().startsWith("ActiveMQ")) {
            return new DestinationFilter(destination) {
               public void send(ProducerBrokerExchange context, Message message) throws Exception {
                  // Send message to Destination
                  if (LOG.isDebugEnabled()) {
                     LOG.debug("SimpleDestinationInterceptor: Sending message to destination:" + this.getActiveMQDestination().getPhysicalName());
                  }
                  // message.setDestination(destination.getActiveMQDestination());
                  super.send(context, message);
               }
            };
         }
         return destination;
      }

      /* (non-Javadoc)
       * @see org.apache.activemq.broker.region.DestinationInterceptor#remove(org.apache.activemq.broker.region.Destination)
       */
      public void remove(Destination destination) {
         LOG.info("remove({})", destination.getName());
         this.broker = null;
      }

      /* (non-Javadoc)
       * @see org.apache.activemq.broker.region.DestinationInterceptor#create(org.apache.activemq.broker.Broker, org.apache.activemq.broker.ConnectionContext, org.apache.activemq.command.ActiveMQDestination)
       */
      public void create(Broker broker, ConnectionContext context, ActiveMQDestination destination) throws Exception {
         LOG.info("create(" + broker.getBrokerName() + ", " + context.toString() + ", " + destination.getPhysicalName());
      }
   }
}
