View Javadoc
1   package org.djutils.rmi;
2   
3   import static org.junit.Assert.assertEquals;
4   import static org.junit.Assert.assertNotEquals;
5   import static org.junit.Assert.assertNotNull;
6   import static org.junit.Assert.fail;
7   
8   import java.net.InetAddress;
9   import java.net.MalformedURLException;
10  import java.net.URL;
11  import java.net.UnknownHostException;
12  import java.rmi.AccessException;
13  import java.rmi.AlreadyBoundException;
14  import java.rmi.NotBoundException;
15  import java.rmi.Remote;
16  import java.rmi.RemoteException;
17  import java.rmi.registry.Registry;
18  import java.util.LinkedHashSet;
19  import java.util.Set;
20  
21  import org.djutils.exceptions.Try;
22  import org.djutils.logger.CategoryLogger;
23  import org.junit.Test;
24  import org.pmw.tinylog.Level;
25  
26  /**
27   * RMITest tests the RMIUtils class and the RMIObject class. Note that port 1099 should be opened for 'localhost' for this test.
28   * <p>
29   * Copyright (c) 2019-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
30   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
31   * <p>
32   * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank">Alexander Verbraeck</a>
33   */
34  public class RMITest
35  {
36      /**
37       * Test the RMI utilities for creating and destroying an RMI registry.
38       * @throws RemoteException on RMI error
39       * @throws AlreadyBoundException when object was already bound (and should not have been there)
40       * @throws NotBoundException when object ould not be found in registry (and should have been there)
41       */
42      @SuppressWarnings("checkstyle:methodlength")
43      @Test
44      public void testRMIRegistry() throws RemoteException, AlreadyBoundException, NotBoundException
45      {
46          CategoryLogger.setAllLogLevel(Level.OFF);
47          // test making the registry
48          Try.testFail(new Try.Execution()
49          {
50              @Override
51              public void execute() throws Throwable
52              {
53                  RMIUtils.getRegistry(null, 1099);
54              }
55          }, "should have thrown NullPointerException", NullPointerException.class);
56          for (int port : new int[] {-1, 0, 100000, -10, 65536})
57          {
58              Try.testFail(new Try.Execution()
59              {
60                  @Override
61                  public void execute() throws Throwable
62                  {
63                      RMIUtils.getRegistry("localhost", port);
64                  }
65              }, "should have thrown IllegalArgumentException", IllegalArgumentException.class);
66          }
67          // the following Try might wait some time for a timeout
68          Try.testFail(new Try.Execution()
69          {
70              @Override
71              public void execute() throws Throwable
72              {
73                  RMIUtils.getRegistry("130.161.3.179", 41099);
74              }
75          }, "should have thrown RemoteException or AccessException", RemoteException.class);
76  
77          // valid registry
78          Registry registry = RMIUtils.getRegistry("localhost", 1099);
79          assertNotNull(registry);
80          assertEquals(0, registry.list().length);
81  
82          // test the bind method
83          String key = "testKey";
84          RemoteObject remoteObject = new RemoteObject(key);
85          Try.testFail(new Try.Execution()
86          {
87              @Override
88              public void execute() throws Throwable
89              {
90                  RMIUtils.bind(null, key, remoteObject);
91              }
92          }, "should have thrown NullPointerException", NullPointerException.class);
93          Try.testFail(new Try.Execution()
94          {
95              @Override
96              public void execute() throws Throwable
97              {
98                  RMIUtils.bind(registry, null, remoteObject);
99              }
100         }, "should have thrown NullPointerException", NullPointerException.class);
101         Try.testFail(new Try.Execution()
102         {
103             @Override
104             public void execute() throws Throwable
105             {
106                 RMIUtils.bind(registry, key, null);
107             }
108         }, "should have thrown NullPointerException", NullPointerException.class);
109         Try.testFail(new Try.Execution()
110         {
111             @Override
112             public void execute() throws Throwable
113             {
114                 RMIUtils.bind(registry, "", remoteObject);
115             }
116         }, "should have thrown IllegalArgumentPointerException", IllegalArgumentException.class);
117 
118         // valid bind
119         RMIUtils.bind(registry, key, remoteObject);
120         assertEquals(1, registry.list().length);
121         Try.testFail(new Try.Execution()
122         {
123             @Override
124             public void execute() throws Throwable
125             {
126                 RMIUtils.bind(registry, key, remoteObject);
127             }
128         }, "should have thrown AlreadyBoundException", AlreadyBoundException.class);
129         assertEquals(1, registry.list().length);
130 
131         // see if the same registry is retrieved the second time
132         Registry reg2 = RMIUtils.getRegistry("localhost", 1099);
133         assertNotNull(reg2);
134         // reg2 and registry can have a different internal structure (IP address versus localhost)
135         // so assertEquals(registry, reg2) cannot be used, but they should still point to the same registry
136         assertEquals(1, reg2.list().length); // see if the two registries are indeed 100% the same
137 
138         // test the lookup method
139         Try.testFail(new Try.Execution()
140         {
141             @Override
142             public void execute() throws Throwable
143             {
144                 RMIUtils.lookup(null, key);
145             }
146         }, "should have thrown NullPointerException", NullPointerException.class);
147         Try.testFail(new Try.Execution()
148         {
149             @Override
150             public void execute() throws Throwable
151             {
152                 RMIUtils.lookup(registry, null);
153             }
154         }, "should have thrown NullPointerException", NullPointerException.class);
155         Try.testFail(new Try.Execution()
156         {
157             @Override
158             public void execute() throws Throwable
159             {
160                 RMIUtils.lookup(registry, "");
161             }
162         }, "should have thrown IllegalArgumentPointerException", IllegalArgumentException.class);
163 
164         // valid lookup
165         RemoteObject ro = (RemoteObject) RMIUtils.lookup(registry, key);
166         assertNotNull(ro);
167         assertEquals(remoteObject, ro);
168         assertEquals(key, ro.getName());
169 
170         // test the rebind method
171         Try.testFail(new Try.Execution()
172         {
173             @Override
174             public void execute() throws Throwable
175             {
176                 RMIUtils.rebind(null, key, remoteObject);
177             }
178         }, "should have thrown NullPointerException", NullPointerException.class);
179         Try.testFail(new Try.Execution()
180         {
181             @Override
182             public void execute() throws Throwable
183             {
184                 RMIUtils.rebind(registry, null, remoteObject);
185             }
186         }, "should have thrown NullPointerException", NullPointerException.class);
187         Try.testFail(new Try.Execution()
188         {
189             @Override
190             public void execute() throws Throwable
191             {
192                 RMIUtils.rebind(registry, key, null);
193             }
194         }, "should have thrown NullPointerException", NullPointerException.class);
195         Try.testFail(new Try.Execution()
196         {
197             @Override
198             public void execute() throws Throwable
199             {
200                 RMIUtils.rebind(registry, "", remoteObject);
201             }
202         }, "should have thrown IllegalArgumentPointerException", IllegalArgumentException.class);
203 
204         // valid rebind
205         String otherKey = "otherKey";
206         RemoteObject otherObject = new RemoteObject(otherKey);
207         assertNotEquals(remoteObject, otherObject);
208         RMIUtils.rebind(registry, key, otherObject);
209         assertEquals(1, registry.list().length);
210         RemoteObject ro2 = (RemoteObject) registry.lookup(key);
211         assertNotNull(ro2);
212         assertEquals(otherObject, ro2);
213         assertNotEquals(key, ro2.getName());
214 
215         // test unbind method
216         Try.testFail(new Try.Execution()
217         {
218             @Override
219             public void execute() throws Throwable
220             {
221                 RMIUtils.unbind(null, key);
222             }
223         }, "should have thrown NullPointerException", NullPointerException.class);
224         Try.testFail(new Try.Execution()
225         {
226             @Override
227             public void execute() throws Throwable
228             {
229                 RMIUtils.unbind(registry, null);
230             }
231         }, "should have thrown NullPointerException", NullPointerException.class);
232         Try.testFail(new Try.Execution()
233         {
234             @Override
235             public void execute() throws Throwable
236             {
237                 RMIUtils.unbind(registry, "");
238             }
239         }, "should have thrown IllegalArgumentPointerException", IllegalArgumentException.class);
240         Try.testFail(new Try.Execution()
241         {
242             @Override
243             public void execute() throws Throwable
244             {
245                 RMIUtils.unbind(registry, otherKey);
246             }
247         }, "should have thrown NotBoundException", NotBoundException.class);
248 
249         // valid rebind / unbind
250         RMIUtils.rebind(registry, otherKey, remoteObject); // rebind should always work
251         assertEquals(2, registry.list().length);
252         RMIUtils.unbind(registry, otherKey);
253         assertEquals(1, registry.list().length);
254 
255         // test closeRegistry
256         RMIUtils.closeRegistry(registry);
257         Try.testFail(new Try.Execution()
258         {
259             @Override
260             public void execute() throws Throwable
261             {
262                 RMIUtils.lookup(registry, key);
263             }
264         }, "should have thrown an Exception");
265         Try.testFail(new Try.Execution()
266         {
267             @Override
268             public void execute() throws Throwable
269             {
270                 RMIUtils.lookup(reg2, key);
271             }
272         });
273 
274         // test that registry is empty after closeRegistry
275         // bind and rebind still work -- closeRegistry takes care it does not accept outside calls anymore
276         Try.testFail(new Try.Execution()
277         {
278             @Override
279             public void execute() throws Throwable
280             {
281                 RMIUtils.lookup(registry, key);
282             }
283         }, "did not get expected exception for lookup()");
284         Try.testFail(new Try.Execution()
285         {
286             @Override
287             public void execute() throws Throwable
288             {
289                 RMIUtils.unbind(registry, key);
290             }
291         }, "did not get expected exception for unbind()");
292         Try.testFail(new Try.Execution()
293         {
294             @Override
295             public void execute() throws Throwable
296             {
297                 RMIUtils.closeRegistry(registry);
298             }
299         }, "did not get expected exception for closeRegistry()");
300 
301         CategoryLogger.setAllLogLevel(Level.INFO);
302     }
303 
304     /**
305      * Test the creation of the RMIRegistry on several versions of localhost (as IP address, using the name, using 127.0.0.1,
306      * and using the localhst string).
307      * @throws UnknownHostException when the IP address or name of the localhost cannot be retrieved
308      */
309     @Test
310     public void testRMIRegistryLocalHost() throws UnknownHostException
311     {
312         CategoryLogger.setAllLogLevel(Level.OFF);
313 
314         for (String localHostName : new String[] {"localhost", "127.0.0.1", InetAddress.getLocalHost().getHostName(),
315                 InetAddress.getLocalHost().getHostAddress()})
316         {
317             Registry registry = null;
318             try
319             {
320                 registry = RMIUtils.getRegistry(localHostName, 1099);
321             }
322             catch (RemoteException exception)
323             {
324                 fail("Creation of local registry failed for host " + localHostName + ", message was: "
325                         + exception.getMessage());
326             }
327 
328             try
329             {
330                 assertNotNull(registry);
331                 assertEquals(0, registry.list().length);
332                 RMIUtils.closeRegistry(registry);
333                 final Registry testRegistry = registry;
334                 Try.testFail(new Try.Execution()
335                 {
336                     @Override
337                     public void execute() throws Throwable
338                     {
339                         RMIUtils.lookup(testRegistry, "key");
340                     }
341                 }, "should have thrown an Exception");
342             }
343             catch (RemoteException exception)
344             {
345                 fail("Access or closing of local registry failed for host " + localHostName + ", RemoteException message was: "
346                         + exception.getMessage());
347             }
348         }
349 
350         CategoryLogger.setAllLogLevel(Level.INFO);
351     }
352 
353     /**
354      * Test the RMIObject class, and test the RMI communication between objects.
355      * @throws AlreadyBoundException when producer is already there
356      * @throws RemoteException on RMI error
357      * @throws NotBoundException when Listener cnnot find producer
358      * @throws MalformedURLException when the test creating RMI regsirt from URL goes wrong
359      */
360     @Test
361     public void testRMIObject() throws RemoteException, AlreadyBoundException, NotBoundException, MalformedURLException
362     {
363         CategoryLogger.setAllLogLevel(Level.OFF);
364 
365         // make the producer
366         Producer producer = new Producer();
367         assertNotNull(producer);
368 
369         // make the listener in another Thread
370         final Listener[] listeners = new Listener[1];
371         listeners[0] = null;
372         Thread thread = new Thread(new Runnable()
373         {
374             @Override
375             public void run()
376             {
377                 Listener listener = null;
378                 try
379                 {
380                     listener = new Listener("listener");
381                 }
382                 catch (RemoteException | AlreadyBoundException | NotBoundException exception)
383                 {
384                     exception.printStackTrace();
385                     fail(exception.getMessage());
386                 }
387                 assertNotNull(listener);
388                 listeners[0] = listener;
389             }
390         });
391         thread.start();
392         int counter = 0;
393         while (listeners[0] == null && counter < 10)
394         {
395             try
396             {
397                 Thread.sleep(100);
398             }
399             catch (InterruptedException e)
400             {
401                 // ignore
402             }
403             counter++;
404         }
405         if (counter >= 10)
406         {
407             fail("Could not start listener");
408         }
409         Listener listener = listeners[0];
410         listener.setLastMessage("");
411         producer.fire("test");
412         assertEquals("test", listener.getLastMessage());
413 
414         // close down
415         Registry registry = producer.getRegistry();
416         RMIUtils.closeRegistry(registry);
417 
418         // check errors in creating RMIObject
419         Try.testFail(new Try.Execution()
420         {
421             @Override
422             public void execute() throws Throwable
423             {
424                 new Producer(null, "producer");
425             }
426         }, "did not get expected exception", NullPointerException.class);
427         Try.testFail(new Try.Execution()
428         {
429             @Override
430             public void execute() throws Throwable
431             {
432                 new Producer(new URL("http://localhost:1099"), null);
433             }
434         }, "did not get expected exception", NullPointerException.class);
435         Try.testFail(new Try.Execution()
436         {
437             @Override
438             public void execute() throws Throwable
439             {
440                 new Producer(null, 1099, "producer");
441             }
442         }, "did not get expected exception", NullPointerException.class);
443         Try.testFail(new Try.Execution()
444         {
445             @Override
446             public void execute() throws Throwable
447             {
448                 new Producer("localhost", -2, "producer");
449             }
450         }, "did not get expected exception", IllegalArgumentException.class);
451         Try.testFail(new Try.Execution()
452         {
453             @Override
454             public void execute() throws Throwable
455             {
456                 new Producer("localhost", 1099, null);
457             }
458         }, "did not get expected exception", NullPointerException.class);
459 
460         // test a few constructions that should work
461         producer = new Producer(new URL("http", null, ""), "producer");
462         assertNotNull(producer);
463         assertNotNull(producer.getRegistry());
464         RMIUtils.closeRegistry(producer.getRegistry());
465         sleep(200);
466         producer = new Producer(new URL("http://127.0.0.1:1099"), "producer");
467         assertNotNull(producer);
468         assertNotNull(producer.getRegistry());
469         RMIUtils.closeRegistry(producer.getRegistry());
470         sleep(200);
471         producer = new Producer(new URL("http://localhost:1099"), "producer");
472         assertNotNull(producer);
473         assertNotNull(producer.getRegistry());
474         RMIUtils.closeRegistry(producer.getRegistry());
475         sleep(200);
476 
477         CategoryLogger.setAllLogLevel(Level.INFO);
478     }
479 
480     /**
481      * @param msec int; the number of msec to sleep.
482      */
483     private static void sleep(final int msec)
484     {
485         try
486         {
487             Thread.sleep(msec);
488         }
489         catch (Exception exception)
490         {
491             // Ignore
492         }
493     }
494 
495     /** A test remote object. */
496     protected static class RemoteObject implements Remote
497     {
498         /** */
499         private final String name;
500 
501         /**
502          * Name a remote object.
503          * @param name the name of the object
504          * @throws RemoteException on a remote exception
505          */
506         public RemoteObject(final String name) throws RemoteException
507         {
508             this.name = name;
509         }
510 
511         /**
512          * @return the name of the object
513          */
514         public String getName()
515         {
516             return this.name;
517         }
518 
519         /** {@inheritDoc} */
520         @Override
521         public int hashCode()
522         {
523             final int prime = 31;
524             int result = 1;
525             result = prime * result + ((this.name == null) ? 0 : this.name.hashCode());
526             return result;
527         }
528 
529         /** {@inheritDoc} */
530         @SuppressWarnings("checkstyle:needbraces")
531         @Override
532         public boolean equals(final Object obj)
533         {
534             if (this == obj)
535                 return true;
536             if (obj == null)
537                 return false;
538             if (getClass() != obj.getClass())
539                 return false;
540             RemoteObject other = (RemoteObject) obj;
541             if (this.name == null)
542             {
543                 if (other.name != null)
544                     return false;
545             }
546             else if (!this.name.equals(other.name))
547                 return false;
548             return true;
549         }
550     }
551 
552     /** ListenerInterface for remote listener object. */
553     public interface ListenerInterface extends Remote
554     {
555         /**
556          * Return the name under which the listenerInterface is registered.
557          * @return the name
558          * @throws RemoteException on network error
559          */
560         String getName() throws RemoteException;
561 
562         /**
563          * Notify the listener of a message.
564          * @param payload the message
565          * @throws RemoteException on network error
566          */
567         void notify(String payload) throws RemoteException;
568     }
569 
570     /** producerInterface for remote producer object. */
571     public interface ProducerInterface extends Remote
572     {
573         /**
574          * Add a listener to the producer.
575          * @param listener the listener to add
576          * @throws RemoteException on network error
577          */
578         void addListener(ListenerInterface listener) throws RemoteException;
579     }
580 
581     /** Producer object. */
582     public class Producer extends RMIObject implements ProducerInterface
583     {
584         /** */
585         private static final long serialVersionUID = 1L;
586 
587         /** */
588         private Set<ListenerInterface> listeners = new LinkedHashSet<>();
589 
590         /**
591          * Register the producer on the localhost RMI registry at the default port.
592          * @throws RemoteException on network error
593          * @throws AlreadyBoundException on error
594          */
595         public Producer() throws RemoteException, AlreadyBoundException
596         {
597             super("localhost", 1099, "producer");
598         }
599 
600         /**
601          * Register this object in the RMI registry.
602          * @param host String; the host where the RMI registry resides or will be created.
603          * @param port int; the port where the RMI registry can be found or will be created
604          * @param bindingKey the key under which this object will be bound in the RMI registry
605          * @throws RemoteException when there is a problem with the RMI registry
606          * @throws AlreadyBoundException when there is already another object bound to the bindingKey
607          * @throws NullPointerException when host, path, or bindingKey is null
608          * @throws IllegalArgumentException when port &lt; 0 or port &gt; 65535
609          * @throws AccessException when there is an attempt to create a registry on a remote host
610          */
611         public Producer(final String host, final int port, final String bindingKey)
612                 throws RemoteException, AlreadyBoundException
613         {
614             super(host, port, bindingKey);
615         }
616 
617         /**
618          * Register this object in the RMI registry.
619          * @param registryURL URL; the URL of the registry, e.g., "http://localhost:1099"
620          * @param bindingKey String; the key under which this object will be bound in the RMI registry
621          * @throws RemoteException when there is a problem with the RMI registry
622          * @throws AlreadyBoundException when there is already another object bound to the bindingKey
623          * @throws NullPointerException when registryURL or bindingKey is null
624          * @throws AccessException when there is an attempt to create a registry on a remote host
625          */
626         public Producer(final URL registryURL, final String bindingKey) throws RemoteException, AlreadyBoundException
627         {
628             super(registryURL, bindingKey);
629         }
630 
631         /** {@inheritDoc} */
632         @Override
633         public void addListener(final ListenerInterface listener) throws RemoteException
634         {
635             this.listeners.add(listener);
636         }
637 
638         /**
639          * Fire a message.
640          * @param payload String the payload to send to the listener
641          * @throws RemoteException on network error
642          */
643         protected void fire(final String payload) throws RemoteException
644         {
645             for (ListenerInterface listener : this.listeners)
646             {
647                 try
648                 {
649                     listener.notify(payload);
650                 }
651                 catch (Exception e)
652                 {
653                     fail("Notifying listener failed");
654                 }
655             }
656         }
657     }
658 
659     /** Listener object. */
660     public class Listener extends RMIObject implements ListenerInterface
661     {
662         /** */
663         private static final long serialVersionUID = 1L;
664 
665         /** listener name. */
666         private final String listenerName;
667 
668         /** last received message. */
669         private String lastMessage;
670 
671         /**
672          * Explicit definition of constructor has to be included to be able to throws RemoteException.
673          * @param listenerName the name of the listener in the registry
674          * @throws RemoteException on network error
675          * @throws AlreadyBoundException on error
676          * @throws NotBoundException when producer cannot be found
677          */
678         public Listener(final String listenerName) throws RemoteException, AlreadyBoundException, NotBoundException
679         {
680             super("localhost", 1099, listenerName);
681             this.listenerName = listenerName;
682             ProducerInterface producer = (ProducerInterface) RMIUtils.lookup(getRegistry(), "producer");
683             producer.addListener(this);
684         }
685 
686         /** {@inheritDoc} */
687         @Override
688         public String getName() throws RemoteException
689         {
690             return this.listenerName;
691         }
692 
693         /** {@inheritDoc} */
694         @Override
695         public void notify(final String payload) throws RemoteException
696         {
697             this.lastMessage = payload;
698         }
699 
700         /**
701          * @return listenerName
702          */
703         public String getListenerName()
704         {
705             return this.listenerName;
706         }
707 
708         /**
709          * @return lastMessage
710          */
711         public String getLastMessage()
712         {
713             return this.lastMessage;
714         }
715 
716         /**
717          * @param lastMessage set lastMessage
718          */
719         public void setLastMessage(final String lastMessage)
720         {
721             this.lastMessage = lastMessage;
722         }
723     }
724 
725 }