/*
 * 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.felix.dm.lambda.itest;
import static org.apache.felix.dm.lambda.DependencyManagerActivator.adapter;
import static org.apache.felix.dm.lambda.DependencyManagerActivator.aspect;
import static org.apache.felix.dm.lambda.DependencyManagerActivator.component;

import java.util.Dictionary;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import org.apache.felix.dm.Component;
import org.apache.felix.dm.DependencyManager;
import org.junit.Assert;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;


/**
 * Test for aspects with service properties propagations.
 * 
 * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
 */
@SuppressWarnings({"rawtypes", "unchecked", "unused"})
public class AspectWithPropagationTest extends TestBase {
    private final static int ASPECTS = 3;
    private final Set<Integer> _randoms = new HashSet<Integer>();
    private final Random _rnd = new Random();
    private static Ensure m_invokeStep;
    private static Ensure m_changeStep;
    
    /**
     * This test does the following:
     * 
     * - Create S service with property "p=s"
     * - Create SA (aspect of S) with property "p=aspect"
     * - Create Client, depending on S (actually, on SA).
     * - Client should see SA with properties p=aspect
     * - Change S service property with "p=smodified": the Client should be changed with SA(p=aspect)
     * - Change aspect service property with "p=aspectmodified": The client should be changed with SA(p=aspectmodified)
     */
    public void testAspectsWithPropagationNotOverriding() {
        System.out.println("----------- Running testAspectsWithPropagationNotOverriding ...");
        DependencyManager m = getDM();
        m_invokeStep = new Ensure(); 
        
        // Create our original "S" service.
        S s = new S() {
			public void invoke() {
			}
        };
		Component sComp = component(m).impl(s).provides(S.class, "p", "s").build();

        // Create SA (aspect of S)
        S sa = new S() {
        	volatile S m_s;
			public void invoke() {
			}
        };
        Component saComp = aspect(m, S.class).rank(1).impl(sa).properties("p", "aspect").build();
                
        // Create client depending on S
        Object client = new Object() {
        	int m_changeCount;
        	void add(Map props, S s) {
        		Assert.assertEquals("aspect", props.get("p"));
        		m_invokeStep.step(1);
        	}
        	
            void change(Map props, S s) {
        		switch (++m_changeCount) {
        		case 1:
        			Assert.assertEquals("aspect", props.get("p"));
            		m_invokeStep.step(2);
            		break;
        		case 2:
        			Assert.assertEquals("aspectmodified", props.get("p"));
            		m_invokeStep.step(3);
        		}
        	}
        };
        Component clientComp = component(m).impl(client).withSvc(S.class, srv->srv.add("add").change("change")).build();
        
        // Add components in dependency manager
        m.add(sComp);
        m.add(saComp);
        m.add(clientComp);
        
        // client should have been added with SA aspect
        m_invokeStep.waitForStep(1, 5000);
        
        // now change s "p=s" to "p=smodified": client should not see it
        Hashtable props = new Hashtable();
        props.put("p", "smodified");
        sComp.setServiceProperties(props);
        m_invokeStep.waitForStep(2, 5000);
        
        // now change sa aspect "p=aspect" to "p=aspectmodified": client should see it
        props = new Hashtable();
        props.put("p", "aspectmodified");
        saComp.setServiceProperties(props);
        m_invokeStep.waitForStep(3, 5000);    
        
        // remove components
        m.remove(clientComp);
        m.remove(saComp);
        m.remove(sComp);
    }
            
    /**
     * This test does the following:
     * 
     * - Create S service
     * - Create some S Aspects
     * - Create a Client, depending on S (actually, on the top-level S aspect)
     * - Client has a "change" callback in order to track S service properties modifications.
     * - First, invoke Client.invoke(): all S aspects, and finally original S service must be invoked orderly.
     * - Modify S original service properties, and check if all aspects, and the client has been orderly called in their "change" callback.
     * - Modify the First lowest ranked aspect (rank=1), and check if all aspects, and client have been orderly called in their "change" callback.
     */
    public void testAspectsWithPropagation() {
        System.out.println("----------- Running testAspectsWithPropagation ...");
        DependencyManager m = getDM();
        // helper class that ensures certain steps get executed in sequence
        m_invokeStep = new Ensure(); 
        
        // Create our original "S" service.
        Dictionary props = new Hashtable();
        props.put("foo", "bar");
        Component s = component(m).impl(new SImpl()).provides(S.class, props).build();
                
        // Create an aspect aware client, depending on "S" service.
        Client clientImpl;
        Component client = component(m).impl((clientImpl = new Client())).withSvc(S.class, srv -> srv.add("add").change("change").remove("remove").swap("swap")).build();     

        // Create some "S" aspects
        Component[] aspects = new Component[ASPECTS];
        for (int rank = 1; rank <= ASPECTS; rank ++) {
            aspects[rank-1] = aspect(m, S.class).rank(rank).impl(new A("A" + rank, rank)).add("add").change("change").remove("remove").swap("swap").build();
            props = new Hashtable();
            props.put("a" + rank, "v" + rank);
            aspects[rank-1].setServiceProperties(props);
        }                                    
              
        // Register client
        m.add(client);
        
        // Randomly register aspects and original service
        boolean originalServiceAdded = false;
        for (int i = 0; i < ASPECTS; i ++) {
            int index = getRandomAspect();
            m.add(aspects[index]);
            if (! originalServiceAdded && _rnd.nextBoolean()) {
                m.add(s);
                originalServiceAdded = true;
            }
        }
        if (! originalServiceAdded) {
            m.add(s);
        }
              
        // All set, check if client has inherited from top level aspect properties + original service properties
        Map check = new HashMap();
        check.put("foo", "bar");
        for (int i = 1; i < (ASPECTS - 1); i ++) {
            check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect.
        }
        check.put("a" + ASPECTS, "v" + ASPECTS);
        checkServiceProperties(check, clientImpl.getServiceProperties());

        // Now invoke client, which orderly calls all aspects in the chain, and finally the original service "S".
        System.out.println("-------------------------- Invoking client.");
        clientImpl.invoke();
        m_invokeStep.waitForStep(ASPECTS+1, 5000);
        
        // Now, change original service "S" properties: this will orderly trigger "change" callbacks on aspects, and on client. 
        System.out.println("-------------------------- Modifying original service properties.");
        m_changeStep = new Ensure();
        props = new Hashtable();
        props.put("foo", "barModified");
        s.setServiceProperties(props);
        
        // Check if aspects and client have been orderly called in their "changed" callback
        m_changeStep.waitForStep(ASPECTS+1, 5000);
        
        // Check if modified "foo" original service property has been propagated
        check = new HashMap();
        check.put("foo", "barModified");
        for (int i = 1; i < (ASPECTS - 1); i ++) {
            check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect.
        }
        check.put("a" + ASPECTS, "v" + ASPECTS); // we only see top-level aspect service properties
        checkServiceProperties(check, clientImpl.getServiceProperties());
        
        // Now, change the top-level ranked aspect: it must propagate to all upper aspects, as well as to the client
        System.out.println("-------------------------- Modifying top-level aspect service properties.");

        m_changeStep = new Ensure();
        for (int i = 1; i <= ASPECTS; i ++) {
            m_changeStep.step(i); // only client has to be changed.
        }
        props = new Hashtable();
        props.put("a" + ASPECTS, "v" + ASPECTS + "-Modified");
        aspects[ASPECTS-1].setServiceProperties(props); // That triggers change callbacks for upper aspects (with rank >= 2)
        m_changeStep.waitForStep(ASPECTS+1, 5000); // check if client have been changed.
        
        // Check if top level aspect service properties have been propagated up to the client.
        check = new HashMap();
        check.put("foo", "barModified");
        for (int i = 1; i < (ASPECTS - 1); i ++) {
            check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect.
        }
        check.put("a" + ASPECTS, "v" + ASPECTS + "-Modified");
        checkServiceProperties(check, clientImpl.getServiceProperties());

        // Clear all components.
        m_changeStep = null;
        m.clear();
    }    
    
    /**
     * This test does the following:
     * 
     * - Create S service
     * - Create some S Aspects without any callbacks (add/change/remove/swap)
     * - Create a Client, depending on S (actually, on the top-level S aspect)
     * - Client has a "change" callack in order to track S service properties modifications.
     * - First, invoke Client.invoke(): all S aspects, and finally original S service must be invoked orderly.
     * - Modify S original service properties, and check if the client has been called in its "change" callback.
     */
    public void testAspectsWithPropagationAndNoCallbacks() {
        System.out.println("----------- Running testAspectsWithPropagation ...");
        DependencyManager m = getDM();
        // helper class that ensures certain steps get executed in sequence
        m_invokeStep = new Ensure(); 
        
        // Create our original "S" service.
        Dictionary props = new Hashtable();
        props.put("foo", "bar");
        Component s = component(m).impl(new SImpl()).provides(S.class, props).build();
        
        // Create an aspect aware client, depending on "S" service.
        Client clientImpl;
        Component client = component(m).impl((clientImpl = new Client())).withSvc(S.class, srv->srv.add("add").change("change").remove("remove")).build();            

        // Create some "S" aspects
        Component[] aspects = new Component[ASPECTS];
        for (int rank = 1; rank <= ASPECTS; rank ++) {
            aspects[rank-1] = aspect(m, S.class).rank(rank).impl(new A("A" + rank, rank)).build();
            props = new Hashtable();
            props.put("a" + rank, "v" + rank);
            aspects[rank-1].setServiceProperties(props);
        }                                    
              
        // Register client
        m.add(client);
        
        // Randomly register aspects and original service
        boolean originalServiceAdded = false;
        for (int i = 0; i < ASPECTS; i ++) {
            int index = getRandomAspect();
            m.add(aspects[index]);
            if (! originalServiceAdded && _rnd.nextBoolean()) {
                m.add(s);
                originalServiceAdded = true;
            }
        }
        if (! originalServiceAdded) {
            m.add(s);
        }
              
        // All set, check if client has inherited from top level aspect properties + original service properties
        Map check = new HashMap();
        check.put("foo", "bar");
        for (int i = 1; i < (ASPECTS - 1); i ++) {
            check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect.
        }
        check.put("a" + ASPECTS, "v" + ASPECTS);
        checkServiceProperties(check, clientImpl.getServiceProperties());

        // Now invoke client, which orderly calls all aspects in the chain, and finally the original service "S".
        System.out.println("-------------------------- Invoking client.");
        clientImpl.invoke();
        m_invokeStep.waitForStep(ASPECTS+1, 5000);
        
        // Now, change original service "S" properties: this will orderly trigger "change" callbacks on aspects, and on client. 
        System.out.println("-------------------------- Modifying original service properties.");
        m_changeStep = new Ensure();
        for (int i = 1; i <= ASPECTS; i ++) {
            m_changeStep.step(i); // skip aspects, which have no "change" callbacks.
        }
        props = new Hashtable();
        props.put("foo", "barModified");
        s.setServiceProperties(props);
        
        // Check if aspects and client have been orderly called in their "changed" callback
        m_changeStep.waitForStep(ASPECTS+1, 5000);
        
        // Check if modified "foo" original service property has been propagated
        check = new HashMap();
        check.put("foo", "barModified");
        for (int i = 1; i < (ASPECTS - 1); i ++) {
            check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect.
        }
        check.put("a" + ASPECTS, "v" + ASPECTS); // we only see top-level aspect service properties
        checkServiceProperties(check, clientImpl.getServiceProperties());
        
        // Clear all components.
        m_changeStep = null;
        m.clear();
    }    
    
    /**
     * This test does the following:
     * 
     * - Create S service
     * - Create some S Aspects
     * - Create S2 Adapter, which adapts S to S2
     * - Create Client2, which depends on S2. Client2 listens to S2 property change events.
     * - Now, invoke Client2.invoke(): all S aspects, and finally original S service must be invoked orderly.
     * - Modify S original service properties, and check if all aspects, S2 Adapter, and Client2 have been orderly called in their "change" callback.
     */
    public void testAdapterWithAspectsAndPropagation() {
        System.out.println("----------- Running testAdapterWithAspectsAndPropagation ...");

        DependencyManager m = getDM();
        m_invokeStep = new Ensure(); 
        
        // Create our original "S" service.
        Dictionary props = new Hashtable();
        props.put("foo", "bar");
        Component s = component(m).impl(new SImpl()).provides(S.class, props).build();
        
        // Create some "S" aspects
        Component[] aspects = new Component[ASPECTS];
        for (int rank = 1; rank <= ASPECTS; rank ++) {
            aspects[rank-1] = aspect(m, S.class).rank(rank).impl(new A("A" + rank, rank)).add("add").change("change").remove("remove").swap("swap").build();
            props = new Hashtable();
            props.put("a" + rank, "v" + rank);
            aspects[rank-1].setServiceProperties(props);
        } 
        
        // Create S2 adapter (which adapts S1 to S2 interface)
        Component adapter = adapter(m, S.class).add("add").change("change").remove("remove").swap("swap").provides(S2.class).impl(new S2Impl()).build();
        
        // Create Client2, which depends on "S2" service.
        Client2 client2Impl;
        Component client2 = component(m).impl((client2Impl = new Client2())).withSvc(S2.class, srv -> srv.add("add").change("change")).build();
              
        // Register client2
        m.add(client2);
        
        // Register S2 adapter
        m.add(adapter);
        
        // Randomly register aspects, original service
        boolean originalServiceAdded = false;
        for (int i = 0; i < ASPECTS; i ++) {
            int index = getRandomAspect();
            m.add(aspects[index]);
            if (! originalServiceAdded && _rnd.nextBoolean()) {
                m.add(s);
                originalServiceAdded = true;
            }
        }
        if (! originalServiceAdded) {
            m.add(s);
        }
             
        // Now invoke client2, which orderly calls all S1 aspects, then S1Impl, and finally S2 service
        System.out.println("-------------------------- Invoking client2.");
        client2Impl.invoke2();
        m_invokeStep.waitForStep(ASPECTS+2, 5000);
        
        // Now, change original service "S" properties: this will orderly trigger "change" callbacks on aspects, S2Impl, and Client2.
        System.out.println("-------------------------- Modifying original service properties.");
        m_changeStep = new Ensure();
        props = new Hashtable();
        props.put("foo", "barModified");
        s.setServiceProperties(props);
        
        // Check if aspects and Client2 have been orderly called in their "changed" callback
        m_changeStep.waitForStep(ASPECTS+2, 5000);
        
        // Check if modified "foo" original service property has been propagated to Client2
        Map check = new HashMap();
        check.put("foo", "barModified");
        for (int i = 1; i < (ASPECTS - 1); i ++) {
            check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect.
        }
        check.put("a" + ASPECTS, "v" + ASPECTS);
        checkServiceProperties(check, client2Impl.getServiceProperties());
        
        // Clear all components.
        m_changeStep = null;
        m.clear();
    }    
    
    /**
     * This test does the following:
     * 
     * - Create S service
     * - Create some S Aspects without any callbacks (add/change/remove)
     * - Create S2 Adapter, which adapts S to S2 (but does not have any add/change/remove callbacks)
     * - Create Client2, which depends on S2. Client2 listens to S2 property change events.
     * - Now, invoke Client2.invoke(): all S aspects, and finally original S service must be invoked orderly.
     * - Modify S original service properties, and check if all aspects, S2 Adapter, and Client2 have been orderly called in their "change" callback.
     */
    public void testAdapterWithAspectsAndPropagationNoCallbacks() {
        System.out.println("----------- Running testAdapterWithAspectsAndPropagationNoCallbacks ...");

        DependencyManager m = getDM();
        m_invokeStep = new Ensure(); 
        
        // Create our original "S" service.
        Dictionary props = new Hashtable();
        props.put("foo", "bar");
        Component s = component(m).impl(new SImpl()).provides(S.class, props).build();
        
        // Create some "S" aspects
        Component[] aspects = new Component[ASPECTS];
        for (int rank = 1; rank <= ASPECTS; rank ++) {
            aspects[rank-1] = aspect(m, S.class).rank(rank).impl(new A("A" + rank, rank)).build();
            props = new Hashtable();
            props.put("a" + rank, "v" + rank);
            aspects[rank-1].setServiceProperties(props);
        } 
        
        // Create S2 adapter (which adapts S1 to S2 interface)
        Component adapter = adapter(m, S.class).provides(S2.class).impl(new S2Impl()).build();                                
        
        // Create Client2, which depends on "S2" service.
        Client2 client2Impl;
        Component client2 = component(m).impl((client2Impl = new Client2())).withSvc(S2.class, srv->srv.add("add").change("change")).build();
              
        // Register client2
        m.add(client2);
        
        // Register S2 adapter
        m.add(adapter);
        
        // Randomly register aspects, original service
        boolean originalServiceAdded = false;
        for (int i = 0; i < ASPECTS; i ++) {
            int index = getRandomAspect();
            m.add(aspects[index]);
            if (! originalServiceAdded && _rnd.nextBoolean()) {
                m.add(s);
                originalServiceAdded = true;
            }
        }
        if (! originalServiceAdded) {
            m.add(s);
        }
             
        // Now invoke client2, which orderly calls all S1 aspects, then S1Impl, and finally S2 service
        System.out.println("-------------------------- Invoking client2.");
        client2Impl.invoke2();
        m_invokeStep.waitForStep(ASPECTS+2, 5000);
        
        // Now, change original service "S" properties: this will orderly trigger "change" callbacks on aspects, S2Impl, and Client2.
        System.out.println("-------------------------- Modifying original service properties.");
        m_changeStep = new Ensure();
        for (int i = 1; i <= ASPECTS+1; i ++) {
            m_changeStep.step(i); // skip all aspects and the adapter
        }
        props = new Hashtable();
        props.put("foo", "barModified");
        s.setServiceProperties(props);
        
        // Check if Client2 has been called in its "changed" callback
        m_changeStep.waitForStep(ASPECTS+2, 5000);
        
        // Check if modified "foo" original service property has been propagated to Client2
        Map check = new HashMap();
        check.put("foo", "barModified");
        for (int i = 1; i < (ASPECTS - 1); i ++) {
            check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect.
        }
        check.put("a" + ASPECTS, "v" + ASPECTS);
        checkServiceProperties(check, client2Impl.getServiceProperties());
        
        // Clear all components.
        m_changeStep = null;
        m.clear();
    }    
    
   private void checkServiceProperties(Map<?, ?> check, Dictionary properties) {
        for (Object key : check.keySet()) {
            Object val = check.get(key);   
            if (val == null) {
                Assert.assertNull(properties.get(key));
            } else {
                Assert.assertEquals(val, properties.get(key));
            }
        }
    }
    
    private int getRandomAspect() {
        int index = 0;  
        do {
            index = _rnd.nextInt(ASPECTS);            
        } while (_randoms.contains(new Integer(index)));
        _randoms.add(new Integer(index));
        return index;
    }

    // S Service
    public static interface S {
        public void invoke();
    }
    
    // S ServiceImpl
    static class SImpl implements S {
        public SImpl() {
        }
        
        public String toString() {
            return "S";
        }
        
        public void invoke() {
             m_invokeStep.step(ASPECTS+1);
        }
    }
    
    // S Aspect
    static class A implements S {
        private final String m_name;
        private volatile ServiceRegistration m_registration;
        private volatile S m_next;
        private final int m_rank;

        public A(String name, int rank) {
            m_name = name;
            m_rank = rank;
        }
        
        public String toString() {
            return m_name;
        }
        
        public void invoke() {
            int rank = ServiceUtil.getRanking(m_registration.getReference());
            m_invokeStep.step(ASPECTS - rank + 1);
            m_next.invoke();
        }
               
        public void add(ServiceReference ref, S s) {
            System.out.println("+++ A" + m_rank + ".add:" + s + "/" + ServiceUtil.toString(ref));   
            m_next = s;
        }

        public void swap(ServiceReference oldSRef, S oldS, ServiceReference newSRef, S newS) {
            System.out.println("+++ A" + m_rank + ".swap: new=" + newS + ", props=" + ServiceUtil.toString(newSRef));
            Assert.assertTrue(m_next == oldS);
            m_next = newS;
        }

        public void change(ServiceReference props, S s) {   
            System.out.println("+++ A" + m_rank + ".change: s=" + s + ", props=" + ServiceUtil.toString(props));
            if (m_changeStep != null) {
                int rank = ServiceUtil.getRanking(m_registration.getReference());
                m_changeStep.step(rank);
            }
        }
        
        public void remove(ServiceReference props, S s) {
            System.out.println("+++ A" + m_rank + ".remove: " + s + ", props=" + ServiceUtil.toString(props));
        }
    }
    
    // Aspect aware client, depending of "S" service aspects.
    static class Client {
        private volatile S m_s;
        private volatile ServiceReference m_sRef;

        public Client() {
        }
        
        public Dictionary getServiceProperties() {
            Dictionary props = new Hashtable();
            for (String key : m_sRef.getPropertyKeys()) {
                props.put(key, m_sRef.getProperty(key));
            }
            return props;
        }

        public void invoke() {
            m_s.invoke();           
        }

        public String toString() {
            return "Client";
        }
        
        public void add(ServiceReference ref, S s) {
            System.out.println("+++ Client.add: " + s + "/" + ServiceUtil.toString(ref));
            m_s = s;
            m_sRef = ref;
        }
              
        public void swap(ServiceReference oldSRef, S oldS, ServiceReference newSRef, S newS) {
            System.out.println("+++ Client.swap: m_s = " + m_s + ", old=" + oldS + ", oldProps=" + ServiceUtil.toString(oldSRef) + ", new=" + newS + ", props=" + ServiceUtil.toString(newSRef));
            Assert.assertTrue(m_s == oldS);
            m_s = newS;
            m_sRef = newSRef;
        }

        public void change(ServiceReference properties, S s) {
            System.out.println("+++ Client.change: s=" + s + ", props=" + ServiceUtil.toString(properties));
            if (m_changeStep != null) {
                m_changeStep.step(ASPECTS+1);
            }
        }
        
        public void remove(ServiceReference props, S s) {
            System.out.println("+++ Client.remove: " + s + ", props=" + ServiceUtil.toString(props));
        }
    }
    
    // S2 Service
    public static interface S2 {
        public void invoke2();
    }

    // S2 impl, which adapts S1 interface to S2 interface
    static class S2Impl implements S2 {
        private volatile S m_s; // we shall see top-level aspect on S service
        
        public void add(ServiceReference ref, S s) {
            System.out.println("+++ S2Impl.add: " + s + "/" + ServiceUtil.toString(ref));
            m_s = s;
        }
              
        public void swap(ServiceReference oldSRef, S oldS, ServiceReference newSRef, S newS) {
            System.out.println("+++ S2Impl.swap: new=" + newS + ", props=" + ServiceUtil.toString(newSRef));
            m_s = newS;
        }

        public void change(ServiceReference properties, S s) {
            System.out.println("+++ S2Impl.change: s=" + s + ", props=" + ServiceUtil.toString(properties));
            if (m_changeStep != null) {
                m_changeStep.step(ASPECTS+1);
            }
        }
        
        public void remove(ServiceReference props, S s) {
            System.out.println("+++ S2Impl.remove: " + s + ", props=" + ServiceUtil.toString(props));
        }
        
        public void invoke2() {
            m_s.invoke();
            m_invokeStep.step(ASPECTS + 2); // All aspects, and S1Impl have been invoked
        }

        public String toString() {
            return "S2";
        }        
    }
    
    // Client2 depending on S2.
    static class Client2 {
        private volatile S2 m_s2;
        private volatile ServiceReference m_s2Ref;

        public Dictionary getServiceProperties() {
            Dictionary props = new Hashtable();
            for (String key : m_s2Ref.getPropertyKeys()) {
                props.put(key, m_s2Ref.getProperty(key));
            }
            return props;
        }

        public void invoke2() {
            m_s2.invoke2();           
        }

        public String toString() {
            return "Client2";
        }  
                
        public void add(ServiceReference ref, S2 s2) {
            System.out.println("+++ Client2.add: " + s2 + "/" + ServiceUtil.toString(ref));
            m_s2 = s2;
            m_s2Ref = ref;
        }

        public void change(ServiceReference props, S2 s2) {   
            System.out.println("+++ Client2.change: s2=" + s2 + ", props=" + ServiceUtil.toString(props));
            if (m_changeStep != null) {
                m_changeStep.step(ASPECTS + 2); // S1Impl, all aspects, and S2 adapters have been changed before us.
            }
        }
    }
}
