/*
 * 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.axis2.jaxws.client;

import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.description.AxisServiceGroup;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.description.WSDL11ToAllAxisServicesBuilder;
import org.apache.axis2.jaxws.description.EndpointDescription;
import org.apache.axis2.jaxws.spi.ClientMetadataTest;

import javax.xml.namespace.QName;
import jakarta.xml.ws.Dispatch;
import jakarta.xml.ws.Service;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import junit.framework.TestCase;

/**
 * Test the serialization of a MessageContext and subsequent deserialization to ensure the
 * JAXWS and Axis2 objects for the relevant ports are hooked back up correctly.  In particluar,
 * make sure the AxisServices can be hooked back up with the MEssageContexts correctly.
 * 
 * This is done as a JAX-WS test rather than an Axis2 test to verify the behavior of ports 
 * added by JAX-WS, particluarly dynamic ports.
 */
public class PortDeserializationTests extends TestCase {
    static final String namespaceURI = "http://client.jaxws.axis2.apache.org";
    static final String svcLocalPart = "svcLocalPart";
    
    static final String dynamicPort = "dynamicPort";
    static final String bindingID = null;
    static final String epr = "http://dummy/endpoint/address";

    /**
     * Validate that a message context can be serialized and deserialized with the Axis2 
     * and JAX-WS description objects not being recreated.  This is similar to a message
     * being serialized and deserialized without the server being stopped.
     */
    public void testDynamicPortSingleServiceNoRecreate() {
        try {
            ClientMetadataTest.installCachingFactory();
            QName svc1QN = new QName(namespaceURI + "?1", svcLocalPart);

            Service svc1 = Service.create(svc1QN);
            QName portQN = new QName(namespaceURI, dynamicPort + "_1");
            svc1.addPort(portQN, bindingID, epr);
            Dispatch<String> port1 = svc1.createDispatch(portQN, String.class, Service.Mode.PAYLOAD);
            
            // We need to get the AxisService so we can set in on a MessageContext and test 
            // serialization / deserialization.  We do this using NON-Public, INTERNAL SPIs
            // since we need to get at the internals of the engine
            org.apache.axis2.jaxws.spi.BindingProvider bindingProvider = 
                (org.apache.axis2.jaxws.spi.BindingProvider) port1;
            EndpointDescription endpointDesc = bindingProvider.getEndpointDescription();
            AxisService axisService = endpointDesc.getAxisService();
            assertNotNull(axisService);
            assertEquals(svc1QN.getLocalPart(), axisService.getName());

            // Now that the AxisService is setup, create an Axis2 message context, set the
            // AxisService on it.  Serialize it out then read it back in.
            MessageContext msgCtx = new MessageContext();
            msgCtx.setAxisService(axisService);
            msgCtx.setAxisServiceGroup(axisService.getAxisServiceGroup());
            ByteArrayOutputStream baos = serializeMessageContext(msgCtx);
            
            // Read in the message context and activate it, which is required by message
            // context deserialization to connect the message context to existing runtime 
            // objects such as AxisService
            MessageContext mcRead = deserializeMessageContext(baos);
            ConfigurationContext configContext = endpointDesc.getServiceDescription().getAxisConfigContext();
            assertNotNull(configContext);
            mcRead.activate(configContext);
            
            AxisService asRead = mcRead.getAxisService();
            assertNotNull(asRead);
            assertEquals(axisService.getName(), asRead.getName());
            assertSame(axisService, asRead);
            AxisServiceGroup agRead = mcRead.getAxisServiceGroup();
            assertNotNull(agRead);
            
            // This keeps the port from being GC'd and causing the AxisService to be freed
            // before the test method completes.
            assertNotNull(port1);

        } catch (Exception t) {
            t.printStackTrace();
            fail("Caught throwable " + t);
        } finally {
            ClientMetadataTest.restoreOriginalFactory();
        }
    }
    
    /**
     * Validate that a message context can be serialized and deserialized with the Axis2 
     * and JAX-WS description objects not being recreated.  This is similar to a message
     * being serialized and deserialized without the server being stopped.
     * 
     * This test uses two services with different namespaces, each with a dynamic port of the
     * same name.
     */
    public void testDynamicPortMultipleServiceNoRecreate() {
        try {
            ClientMetadataTest.installCachingFactory();
            QName svc1QN = new QName(namespaceURI + "?1", svcLocalPart);
            QName svc2QN = new QName(namespaceURI + "?2", svcLocalPart);

            Service svc1 = Service.create(svc1QN);
            Service svc2 = Service.create(svc2QN);

            // Create the same port under the two different services.  Each port gets a unique
            // EPR so it will cause a new port to be created (rather than shared) which in 
            // turn causes a new AxisService to be crated.
            QName portQN = new QName(namespaceURI, dynamicPort + "_1");
            svc1.addPort(portQN, bindingID, epr + "1");
            Dispatch<String> port1 = svc1.createDispatch(portQN, String.class, Service.Mode.PAYLOAD);
            svc2.addPort(portQN, bindingID, epr + "2");
            Dispatch<String> port2 = svc2.createDispatch(portQN, String.class, Service.Mode.PAYLOAD);
            
            // We need to get the AxisService so we can set in on a MessageContext and test 
            // serialization / deserialization.  We do this using NON-Public, INTERNAL SPIs
            // since we need to get at the internals of the engine
            
            // Check the AxisService created for both ports.
            org.apache.axis2.jaxws.spi.BindingProvider bindingProvider1 = 
                (org.apache.axis2.jaxws.spi.BindingProvider) port1;
            EndpointDescription endpointDesc1 = bindingProvider1.getEndpointDescription();
            AxisService axisService1 = endpointDesc1.getAxisService();
            assertNotNull(axisService1);
            assertEquals(svc1QN.getLocalPart(), axisService1.getName());
            
            org.apache.axis2.jaxws.spi.BindingProvider bindingProvider2 = 
                (org.apache.axis2.jaxws.spi.BindingProvider) port2;
            EndpointDescription endpointDesc2 = bindingProvider2.getEndpointDescription();
            AxisService axisService2 = endpointDesc2.getAxisService();
            assertNotNull(axisService2);
            assertNotSame(axisService1, axisService2);
            // The 2nd AxisService created gets a unique ID appended to the name
            String baseName = svc2QN.getLocalPart();
            assertFalse(baseName.equals(axisService2.getName()));
            assertTrue(axisService2.getName().startsWith(baseName));

            // Now that the AxisService is setup, create two Axis2 message contexts, set the
            // AxisServices on them.  Serialize them out
            MessageContext msgCtx1 = new MessageContext();
            msgCtx1.setAxisService(axisService1);
            msgCtx1.setAxisServiceGroup(axisService1.getAxisServiceGroup());
            ByteArrayOutputStream baos1 = serializeMessageContext(msgCtx1);
            
            MessageContext msgCtx2 = new MessageContext();
            msgCtx2.setAxisService(axisService2);
            msgCtx2.setAxisServiceGroup(axisService2.getAxisServiceGroup());
            ByteArrayOutputStream baos2 = serializeMessageContext(msgCtx2);
            
            // Read in the message contexts and activate them, which is required by message
            // context deserialization to connect the message context to existing runtime 
            // objects such as AxisService
            // Note that we do them in reverse order to make sure the logic is order-independent
            
            // Do the same for the second Message Context
            MessageContext mcRead2 = deserializeMessageContext(baos2);
            ConfigurationContext configContext2 = endpointDesc2.getServiceDescription().getAxisConfigContext();
            assertNotNull(configContext2);
            mcRead2.activate(configContext2);
            AxisService asRead2 = mcRead2.getAxisService();
            assertNotNull(asRead2);
            assertEquals(axisService2.getName(), asRead2.getName());
            assertSame(axisService2, asRead2);
            AxisServiceGroup agRead2 = mcRead2.getAxisServiceGroup();
            assertNotNull(agRead2);

            MessageContext mcRead1 = deserializeMessageContext(baos1);
            ConfigurationContext configContext1 = endpointDesc1.getServiceDescription().getAxisConfigContext();
            assertSame(configContext1, configContext2);
            assertNotNull(configContext1);
            mcRead1.activate(configContext1);
            AxisService asRead1 = mcRead1.getAxisService();
            assertNotNull(asRead1);
            assertEquals(axisService1.getName(), asRead1.getName());
            assertSame(axisService1, asRead1);
            AxisServiceGroup agRead1 = mcRead1.getAxisServiceGroup();
            assertNotNull(agRead1);

            
            // These keep the ports from being GC'd before the test method completes and
            // freeing up the AxisServices
            assertNotNull(port1);
            assertNotNull(port2);
            
        } catch (Exception t) {
            t.printStackTrace();
            fail("Caught throwable " + t);
        } finally {
            ClientMetadataTest.restoreOriginalFactory();
        }
    }
    /**
     * Validate that a message context can be serialized and deserialized with the Axis2 
     * and JAX-WS description objects not being recreated.  This is similar to a message
     * being serialized and deserialized without the server being stopped.
     * 
     * This test uses two services with the same namespaces, each with a dynamic port of the
     * same name.
     */
    public void testDynamicPortMultipleServiceNoRecreateSameNS() {
        try {
            ClientMetadataTest.installCachingFactory();
            QName svc1QN = new QName(namespaceURI + "?1", svcLocalPart);
            QName svc2QN = new QName(namespaceURI + "?1", svcLocalPart);

            Service svc1 = Service.create(svc1QN);
            Service svc2 = Service.create(svc2QN);

            // Create the same port under the two different services.  Each port gets a unique
            // EPR so it will cause a new port to be created (rather than shared) which in 
            // turn causes a new AxisService to be crated.
            QName portQN = new QName(namespaceURI, dynamicPort + "_1");
            svc1.addPort(portQN, bindingID, epr + "1");
            Dispatch<String> port1 = svc1.createDispatch(portQN, String.class, Service.Mode.PAYLOAD);
            svc2.addPort(portQN, bindingID, epr + "2");
            Dispatch<String> port2 = svc2.createDispatch(portQN, String.class, Service.Mode.PAYLOAD);
            
            // We need to get the AxisService so we can set in on a MessageContext and test 
            // serialization / deserialization.  We do this using NON-Public, INTERNAL SPIs
            // since we need to get at the internals of the engine
            
            // Check the AxisService created for both ports.
            org.apache.axis2.jaxws.spi.BindingProvider bindingProvider1 = 
                (org.apache.axis2.jaxws.spi.BindingProvider) port1;
            EndpointDescription endpointDesc1 = bindingProvider1.getEndpointDescription();
            AxisService axisService1 = endpointDesc1.getAxisService();
            assertNotNull(axisService1);
            assertEquals(svc1QN.getLocalPart(), axisService1.getName());
            
            org.apache.axis2.jaxws.spi.BindingProvider bindingProvider2 = 
                (org.apache.axis2.jaxws.spi.BindingProvider) port2;
            EndpointDescription endpointDesc2 = bindingProvider2.getEndpointDescription();
            AxisService axisService2 = endpointDesc2.getAxisService();
            assertNotNull(axisService2);
            assertNotSame(axisService1, axisService2);
            // The 2nd AxisService created gets a unique ID appended to the name
            String baseName = svc2QN.getLocalPart();
            assertFalse(baseName.equals(axisService2.getName()));
            assertTrue(axisService2.getName().startsWith(baseName));
            
            // Now that the AxisService is setup, create two Axis2 message contexts, set the
            // AxisServices on them.  Serialize them out
            MessageContext msgCtx1 = new MessageContext();
            msgCtx1.setAxisService(axisService1);
            msgCtx1.setAxisServiceGroup(axisService1.getAxisServiceGroup());
            ByteArrayOutputStream baos1 = serializeMessageContext(msgCtx1);
            
            MessageContext msgCtx2 = new MessageContext();
            msgCtx2.setAxisService(axisService2);
            msgCtx2.setAxisServiceGroup(axisService2.getAxisServiceGroup());
            ByteArrayOutputStream baos2 = serializeMessageContext(msgCtx2);
            
            // Read in the message contexts and activate them, which is required by message
            // context deserialization to connect the message context to existing runtime 
            // objects such as AxisService
            MessageContext mcRead1 = deserializeMessageContext(baos1);
            ConfigurationContext configContext1 = endpointDesc1.getServiceDescription().getAxisConfigContext();
            assertNotNull(configContext1);
            mcRead1.activate(configContext1);
            AxisService asRead1 = mcRead1.getAxisService();
            assertNotNull(asRead1);
            assertEquals(axisService1.getName(), asRead1.getName());
            assertSame(axisService1, asRead1);
            AxisServiceGroup agRead1 = mcRead1.getAxisServiceGroup();
            assertNotNull(agRead1);
           
            // Do the same for the second Message Context
            MessageContext mcRead2 = deserializeMessageContext(baos2);
            ConfigurationContext configContext2 = endpointDesc2.getServiceDescription().getAxisConfigContext();
            assertSame(configContext1, configContext2);
            assertNotNull(configContext2);
            mcRead2.activate(configContext2);
            AxisService asRead2 = mcRead2.getAxisService();
            assertNotNull(asRead2);
            assertEquals(axisService2.getName(), asRead2.getName());
            assertSame(axisService2, asRead2);
            AxisServiceGroup agRead2 = mcRead2.getAxisServiceGroup();
            assertNotNull(agRead2);

            // These keep the ports from being GC'd before the test method completes and
            // freeing up the AxisServices
            assertNotNull(port1);
            assertNotNull(port2);
            
        } catch (Exception t) {
            t.printStackTrace();
            fail("Caught throwable " + t);
        } finally {
            ClientMetadataTest.restoreOriginalFactory();
        }
    }

    /**
     * Validate that a message context can be serialized and deserialized with the Axis2 
     * and JAX-WS description objects being recreated before the deserialization.  This is 
     * similar to a message being serialized, the server being restarted and then the 
     * messgage being deserialized.  In this case it must be hooked up with new instances 
     * of what should be identical objects such as AxisServices.
     */
    public void testDynamicPortMultipleServiceWithRecreate() {
        try {
            ClientMetadataTest.installCachingFactory();
            QName svc1QN = new QName(namespaceURI + "?1", svcLocalPart);
            QName svc2QN = new QName(namespaceURI + "?2", svcLocalPart);
            QName portQN = new QName(namespaceURI, dynamicPort + "_1");
            
            Service svc1 = Service.create(svc1QN);
            Service svc2 = Service.create(svc2QN);

            // Create the same port under the two different services.  Each port gets a unique
            // EPR so it will cause a new port to be created (rather than shared) which in 
            // turn causes a new AxisService to be crated.
            svc1.addPort(portQN, bindingID, epr + "1");
            Dispatch<String> port1 = svc1.createDispatch(portQN, String.class, Service.Mode.PAYLOAD);
            svc2.addPort(portQN, bindingID, epr + "2");
            Dispatch<String> port2 = svc2.createDispatch(portQN, String.class, Service.Mode.PAYLOAD);
            
            // We need to get the AxisService so we can set in on a MessageContext and test 
            // serialization / deserialization.  We do this using NON-Public, INTERNAL SPIs
            // since we need to get at the internals of the engine
            
            // Check the AxisService created for both ports.
            org.apache.axis2.jaxws.spi.BindingProvider bindingProvider1 = 
                (org.apache.axis2.jaxws.spi.BindingProvider) port1;
            EndpointDescription endpointDesc1 = bindingProvider1.getEndpointDescription();
            AxisService axisService1 = endpointDesc1.getAxisService();
            assertNotNull(axisService1);
            assertEquals(svc1QN.getLocalPart(), axisService1.getName());
            
            org.apache.axis2.jaxws.spi.BindingProvider bindingProvider2 = 
                (org.apache.axis2.jaxws.spi.BindingProvider) port2;
            EndpointDescription endpointDesc2 = bindingProvider2.getEndpointDescription();
            AxisService axisService2 = endpointDesc2.getAxisService();
            assertNotNull(axisService2);
            assertNotSame(axisService1, axisService2);
            // The 2nd AxisService created gets a unique ID appended to the name
            String baseName = svc2QN.getLocalPart();
            assertFalse(baseName.equals(axisService2.getName()));
            assertTrue(axisService2.getName().startsWith(baseName));
            
            // Now that the AxisService is setup, create two Axis2 message contexts, set the
            // AxisServices on them.  Serialize them out
            MessageContext msgCtx1 = new MessageContext();
            msgCtx1.setAxisService(axisService1);
            msgCtx1.setAxisServiceGroup(axisService1.getAxisServiceGroup());
            ByteArrayOutputStream baos1 = serializeMessageContext(msgCtx1);
            
            MessageContext msgCtx2 = new MessageContext();
            msgCtx2.setAxisService(axisService2);
            msgCtx2.setAxisServiceGroup(axisService2.getAxisServiceGroup());
            ByteArrayOutputStream baos2 = serializeMessageContext(msgCtx2);
            
            // Now cause the runtime objects to be recreated.  Do this by forcing a release
            // of both Service instances, which will also release the runtime objects
            // such as the AxisService underneath them.  Then recreate the ports
            org.apache.axis2.jaxws.spi.ServiceDelegate.releaseService(svc1);
            org.apache.axis2.jaxws.spi.ServiceDelegate.releaseService(svc2);
        
            Service svc1_redo = Service.create(svc1QN);
            Service svc2_redo = Service.create(svc2QN);
            
            svc1_redo.addPort(portQN, bindingID, epr + "1");
            Dispatch<String> port1_redo = svc1_redo.createDispatch(portQN, String.class, Service.Mode.PAYLOAD);
            svc2_redo.addPort(portQN, bindingID, epr + "2");
            Dispatch<String> port2_redo = svc2_redo.createDispatch(portQN, String.class, Service.Mode.PAYLOAD);

            org.apache.axis2.jaxws.spi.BindingProvider bindingProvider1_redo = 
                (org.apache.axis2.jaxws.spi.BindingProvider) port1_redo;
            EndpointDescription endpointDesc1_redo = bindingProvider1_redo.getEndpointDescription();
            AxisService axisService1_redo = endpointDesc1_redo.getAxisService();
            assertNotNull(axisService1_redo);
            assertEquals(baseName, axisService1_redo.getName());
            
            org.apache.axis2.jaxws.spi.BindingProvider bindingProvider2_redo = 
                (org.apache.axis2.jaxws.spi.BindingProvider) port2_redo;
            EndpointDescription endpointDesc2_redo = bindingProvider2_redo.getEndpointDescription();
            AxisService axisService2_redo = endpointDesc2_redo.getAxisService();
            assertNotNull(axisService2_redo);
            assertNotSame(axisService1_redo, axisService2_redo);
            Parameter svcQNParam1_redo = axisService1_redo.getParameter(WSDL11ToAllAxisServicesBuilder.WSDL_SERVICE_QNAME);
            assertEquals(svc1QN, svcQNParam1_redo.getValue());
            // The 2nd AxisService created gets a unique ID appended to the name
            assertFalse(baseName.equals(axisService2_redo.getName()));
            assertTrue(axisService2_redo.getName().startsWith(baseName));
            Parameter svcQNParam2_redo = axisService2_redo.getParameter(WSDL11ToAllAxisServicesBuilder.WSDL_SERVICE_QNAME);
            assertEquals(svc2QN, svcQNParam2_redo.getValue());

            // Read in the message contexts and activate them, which is required by message
            // context deserialization to connect the message context to existing runtime 
            // objects such as AxisService
            MessageContext mcRead1 = deserializeMessageContext(baos1);
            ConfigurationContext configContext1 = endpointDesc1_redo.getServiceDescription().getAxisConfigContext();
            assertNotNull(configContext1);
            mcRead1.activate(configContext1);
            AxisService asRead1 = mcRead1.getAxisService();
            assertNotNull(asRead1);
            assertEquals(axisService1_redo.getName(), asRead1.getName());
            assertSame(axisService1_redo, asRead1);
            AxisServiceGroup agRead1 = mcRead1.getAxisServiceGroup();
            assertNotNull(agRead1);

            
            // Do the same for the second Message Context
            MessageContext mcRead2 = deserializeMessageContext(baos2);
            ConfigurationContext configContext2 = endpointDesc2_redo.getServiceDescription().getAxisConfigContext();
            assertNotNull(configContext2);
            assertSame(configContext1, configContext2);
            mcRead2.activate(configContext2);
            AxisService asRead2 = mcRead2.getAxisService();
            assertNotNull("AxisService was not activated", asRead2);
            assertEquals(axisService2_redo.getName(), asRead2.getName());
            assertSame(axisService2_redo, asRead2);
            AxisServiceGroup agRead2 = mcRead2.getAxisServiceGroup();
            assertNotNull("AxisServiceGroup was not activated", agRead2);
            
            // These keep the ports from being GC'd before the test method completes and
            // freeing up the AxisServices
            assertNotNull(port1);
            assertNotNull(port2);
            assertNotNull(port1_redo);
            assertNotNull(port2_redo);
            
        } catch (Exception t) {
            t.printStackTrace();
            fail("Caught throwable " + t);
        } finally {
            ClientMetadataTest.restoreOriginalFactory();
        }
    }


    /**
     * Deserialize a message context from the stream.  IMPORTANT NOTE!  After the 
     * message context is deserialized, the activate(...) method must be called on it.
     * @param baos
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private MessageContext deserializeMessageContext(ByteArrayOutputStream baos)
            throws IOException, ClassNotFoundException {
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        
        MessageContext mcRead = new MessageContext();
        mcRead.readExternal(ois);
        return mcRead;
    }

    private ByteArrayOutputStream serializeMessageContext(MessageContext msgCtx)
            throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        msgCtx.writeExternal(oos);
        oos.flush();
        oos.close();
        return baos;
    }

}
