001/*
002 * ============================================================================
003 * Copyright © 2002-2026 by Thomas Thrien.
004 * All Rights Reserved.
005 * ============================================================================
006 * Licensed to the public under the agreements of the GNU Lesser General Public
007 * License, version 3.0 (the "License"). You may obtain a copy of the License at
008 *
009 *      http://www.gnu.org/licenses/lgpl.html
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014 * License for the specific language governing permissions and limitations
015 * under the License.
016 */
017
018package org.tquadrat.foundation.mgmt;
019
020import static java.lang.String.format;
021import static java.rmi.registry.LocateRegistry.createRegistry;
022import static java.rmi.registry.LocateRegistry.getRegistry;
023import static javax.management.remote.JMXConnectorServerFactory.newJMXConnectorServer;
024import static org.apiguardian.api.API.Status.INTERNAL;
025import static org.apiguardian.api.API.Status.STABLE;
026import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_CHARSEQUENCE;
027import static org.tquadrat.foundation.lang.DebugOutput.ifDebug;
028import static org.tquadrat.foundation.lang.Objects.isNull;
029import static org.tquadrat.foundation.lang.Objects.nonNull;
030import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
031import static org.tquadrat.foundation.lang.Objects.requireNotBlankArgument;
032import static org.tquadrat.foundation.lang.Objects.requireNotEmptyArgument;
033
034import javax.management.InstanceNotFoundException;
035import javax.management.MBeanRegistrationException;
036import javax.management.MBeanServer;
037import javax.management.MalformedObjectNameException;
038import javax.management.ObjectName;
039import javax.management.remote.JMXConnectorServer;
040import javax.management.remote.JMXServiceURL;
041import java.io.IOException;
042import java.net.MalformedURLException;
043import java.rmi.RemoteException;
044import java.rmi.registry.Registry;
045import java.util.ArrayList;
046import java.util.HashMap;
047import java.util.List;
048import java.util.Map;
049import java.util.StringJoiner;
050
051import org.apiguardian.api.API;
052import org.tquadrat.foundation.annotation.ClassVersion;
053import org.tquadrat.foundation.annotation.UtilityClass;
054import org.tquadrat.foundation.exception.PrivateConstructorForStaticClassCalledError;
055import org.tquadrat.foundation.exception.UnexpectedExceptionError;
056import org.tquadrat.foundation.lang.NameValuePair;
057
058/**
059 *  This class provides some utilities that are useful in the context of JMX.
060 *
061 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
062 *  @version $Id: JMXUtils.java 1258 2026-06-04 18:33:06Z tquadrat $
063 *  @since 0.0.1
064 *
065 *  @UMLGraph.link
066 */
067@UtilityClass
068@ClassVersion( sourceVersion = "$Id: JMXUtils.java 1258 2026-06-04 18:33:06Z tquadrat $" )
069@API( status = STABLE, since = "0.0.1" )
070public final class JMXUtils
071{
072        /*-----------*\
073    ====** Constants **========================================================
074        \*-----------*/
075    /**
076     *  <p>{@summary The JNDI name for the exposed
077     *  {@link MBeanServer}: {@value}}</p>
078     *
079     *  @see #enableRemoteAccess(MBeanServer,JMXServiceURL,int,Map)
080     *  @see #enableRemoteAccess(MBeanServer,int,Map)
081     *  @see #enableRemoteAccess(MBeanServer,String,int,int,Map)
082     *  @see #disableRemoteAccess(JMXServiceURL)
083     */
084    public static final String BIND_NAME = "jmxrmi";
085
086    /**
087     *  The property name for the connector address: {@value}.
088     */
089    public static final String CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress";
090
091    /**
092     *  The name of the JMX domain that is used by all JMX enabled components
093     *  of the library.
094     */
095    public static final String JMX_DOMAIN = "org.tquadrat";
096
097    /**
098     *  The property name for the class of an MBean: {@value}.
099     */
100    public static final String MBEAN_CLASS = "class";
101
102    /**
103     *  The property name for the function of an MBean: {@value}.
104     */
105    public static final String MBEAN_FUNCTION = "function";
106
107    /**
108     *  The property name for the loader of an MBean: {@value}.
109     */
110    public static final String MBEAN_LOADER = "loader";
111
112    /**
113     *  The property name for the name of an MBean: {@value}.
114     */
115    public static final String MBEAN_NAME = "name";
116
117    /**
118     *  The property name for the MBean type: {@value}.
119     */
120    public static final String MBEAN_TYPE = "type";
121
122        /*------------*\
123    ====** Attributes **=======================================================
124        \*------------*/
125    /**
126     *  The
127     *  {@link javax.management.remote.JMXConnectorServer}
128     *  instance that expose an
129     *  {@link MBeanServer}.
130     *
131     *  @see #enableRemoteAccess(MBeanServer,int,Map)
132     */
133    private static final Map<JMXServiceURL,JMXConnectorServer> m_ConnectorServers = new HashMap<>();
134
135        /*--------------*\
136    ====** Constructors **=====================================================
137        \*--------------*/
138    /**
139     *  No instance allowed for this class.
140     */
141    private JMXUtils() { throw new PrivateConstructorForStaticClassCalledError( JMXUtils.class ); }
142
143        /*---------*\
144    ====** Methods **==========================================================
145        \*---------*/
146    /**
147     *  <p>{@summary Composes an object name from the given domain name and the
148     *  given properties.}</p>
149     *  <p>The object name has the form</p>
150     *  <pre><code>    &lt;Domain&gt;:type=&lt;Type&gt;,function=&lt;Function&gt;<b>[</b>, class=&lt;Class&gt;<b>]</b><b>[</b>,…<b>]</b></code></pre>
151     *  <p>The type is something like a category.</p>
152     *  <p>The function is a description for what the MBean does.</p>
153     *  <p>The class can be provided, if multiple MBean implementations with
154     *  the same type and function will be loaded.</p>
155     *  <p>Additional properties in the form
156     *  <code>&lt;name&gt;=&lt;value&gt;</code> can be added as required.</p>
157     *
158     *  @param  domainName  The domain name.
159     *  @param  type    The type of the MBean that will be named with the new
160     *      object name.
161     *  @param  function    The function of the MBean.
162     *  @param  mbeanClass  The MBean's class; can be {@null}.
163     *  @param  properties  Additional properties as name-value-pairs; can be
164     *      {@null}.
165     *  @return The object name.
166     *  @throws MalformedObjectNameException    It is not possible to create a
167     *      valid object name from the given domain name and properties.
168     */
169    @SafeVarargs
170    @API( status = STABLE, since = "0.0.1" )
171    public static ObjectName composeObjectName( final String domainName, final String type, final String function, final Class<?> mbeanClass, final NameValuePair<String>... properties ) throws MalformedObjectNameException
172    {
173        final var propertyList = new ArrayList<NameValuePair<String>> ();
174        if( nonNull( properties ) ) propertyList.addAll( List.of( properties ) );
175        if( nonNull( mbeanClass ) ) propertyList.add( new NameValuePair<>( MBEAN_CLASS, mbeanClass.getName() ) );
176        propertyList.add( new NameValuePair<>( MBEAN_FUNCTION, requireNotEmptyArgument( function, "function" ) ) );
177        propertyList.add( new NameValuePair<>( MBEAN_TYPE, requireNotEmptyArgument( type, "type" ) ) );
178
179        @SuppressWarnings( "unchecked" )
180        final var retValue = composeObjectName( domainName, propertyList.toArray( NameValuePair []::new ) );
181
182        //---* Done *----------------------------------------------------------
183        return retValue;
184    }   //  composeObjectName()
185
186    /**
187     *  Composes an object name from the given domain name and the given
188     *  properties.
189     *
190     *  @param  domainName  The domain name.
191     *  @param  properties  The properties as name-value-pairs; at least one
192     *      property has to be provided.
193     *  @return The object name.
194     *  @throws MalformedObjectNameException    It is not possible to create a
195     *      valid object name from the given domain name and properties.
196     */
197    @SafeVarargs
198    @API( status = STABLE, since = "0.0.1" )
199    public static ObjectName composeObjectName( final String domainName, final NameValuePair<String>... properties ) throws MalformedObjectNameException
200    {
201        final var name = new StringJoiner( ",", format( "%s:", requireNotEmptyArgument( domainName, "domainName" ) ), EMPTY_CHARSEQUENCE );
202        for( final var property : requireNotEmptyArgument( properties, "properties" ) )
203        {
204            name.add( toString( property ) );
205        }
206        final var retValue = new ObjectName( name.toString() );
207
208        //---* Done *----------------------------------------------------------
209        return retValue;
210    }   //  composeObjectName()
211
212    /**
213     *  <p>{@summary Composes an instance of
214     *  {@link JMXServiceURL}
215     *  as ist is needed to expose an MBean server or to connect to an
216     *  exposed MBean server.}</p>
217     *  <p>This version creates a URL that can be used for local connections
218     *  (both processes are running on the same machine).</p>
219     *
220     *  @param  registryPort    The number of the registry port.
221     *  @return The service URL.
222     *
223     *  @see #BIND_NAME
224     *
225     *  @since 0.25.3
226     */
227    @API( status = STABLE, since = "0.25.3" )
228    public static final JMXServiceURL composeServiceURL( final int registryPort )
229    {
230        final JMXServiceURL retValue;
231        try
232        {
233            retValue = new JMXServiceURL( "service:jmx:rmi:///jndi/rmi://localhost:%2$d/%1$s".formatted( BIND_NAME, registryPort ) );
234        }
235        catch( MalformedURLException e )
236        {
237            throw new UnexpectedExceptionError( e );
238        }
239
240        //---* Done *----------------------------------------------------------
241        return retValue;
242    }   //  composeServiceURL()
243
244    /**
245     *  <p>{@summary Composes an instance of
246     *  {@link JMXServiceURL}
247     *  as ist is needed to expose an MBean server or to connect to an
248     *  exposed MBean server.}</p>
249     *  <p>This version creates a URL that can be used for remote connections
250     *  (the processes are running on different machines).</p>
251     *
252     *  @param  hostName    The host name.
253     *  @param  registryPort    The number of the registry port.
254     *  @param  dataPort    The number of the data port; can be the same as the
255     *      registry port.
256     *  @return The service URL.
257     *  @throws MalformedURLException   It is not possible to compose a valid
258     *      {@link JMXServiceURL}
259     *      with the given {@code hostName}.
260     *
261     *  @see #BIND_NAME
262     *
263     *  @since 0.25.3
264     */
265    @API( status = STABLE, since = "0.25.3" )
266    public static final JMXServiceURL composeServiceURL( final String hostName, final int registryPort, final int dataPort ) throws MalformedURLException
267    {
268        final var retValue = new JMXServiceURL( "service:jmx:rmi://%2$s:%4$d/jndi/rmi://%2$s:%3$d/%1$s".formatted( BIND_NAME, requireNotBlankArgument( hostName, "hostName" ), registryPort, dataPort ) );
269
270        //---* Done *----------------------------------------------------------
271        return retValue;
272    }   //  composeServiceURL()
273
274    /**
275     *  <p>{@summary Disables the external access to the
276     *  {@link MBeanServer}
277     *  for the given service URL.} Nothing happens if there was no NBean
278     *  server exposed that URL.</p>
279     *  <p>Internally, this method deactivates the
280     *  {@linkplain JMXConnectorServer connector server},
281     *  that is, stops listening for client connections. Calling this method
282     *  will also close all client connections that were made by this server.
283     *  After this method returns, whether normally or with an exception, the
284     *  connector server will not create any new client connections.</p>
285     *  <p>Once a connector server has been stopped, it cannot be started
286     *  again.</p>
287     *  <p>Calling this method when the connector server has already been
288     *  stopped has also no effect.</p>
289     *  <p>If closing a client connection produces an exception, that
290     *  exception is not thrown from this method. A
291     *  {@link javax.management.remote.JMXConnectionNotification JMXConnectionNotification}
292     *  with type
293     *  {@link javax.management.remote.JMXConnectionNotification#FAILED JMXConnectionNotification.FAILED}
294     *  is emitted from this MBean with the connection ID of the connection
295     *  that could not be closed.</p>
296     *  <p>Closing a connector server is a potentially slow operation. For
297     *  example, if a client machine with an open connection has crashed, the
298     *  close operation might have to wait for a network protocol timeout.
299     *  Callers that do not want to block in a close operation should do it in
300     *  a separate thread.</p>
301     *  <p>This method works for both locally and remotely exposed connector
302     *  servers.</p>
303     *
304     *  @param  serviceURL  The URL that is used to connect to the MBean
305     *      server.
306     *  @throws IOException The connection server cannot be closed cleanly.
307     *      When this exception is thrown, the connection server has already
308     *      attempted to close all client connections. All client connections
309     *      are closed except possibly those that generated exceptions when the
310     *      server attempted to close them.
311     *  @since 0.25.3
312     */
313    @API( status = STABLE, since = "0.25.3" )
314    public static final void disableRemoteAccess( final JMXServiceURL serviceURL ) throws IOException
315    {
316        final JMXConnectorServer connectorServer;
317        synchronized( m_ConnectorServers )
318        {
319            connectorServer = m_ConnectorServers.remove( requireNonNullArgument( serviceURL, "serviceURL" ) );
320        }
321        if( nonNull( connectorServer ) )
322        {
323            /*
324             * Stopping the connector server will also unbind it from the
325             * registry automatically. We do not need to do explicitly here.
326             */
327            connectorServer.stop();
328        }
329    }   //  disableRemoteAccess()
330
331    /**
332     *  <p>{@summary Enables the external access to the
333     *  {@link MBeanServer}.}</p>
334     *  <p>{@link #enableRemoteAccess(MBeanServer,String,int,int,Map)}
335     *  and
336     *  {@link #enableRemoteAccess(MBeanServer,int,Map)}
337     *  are delegating to this method. See there for details.</p>
338     *
339     *  @param  mbeanServer The MBean server that should be exposed.
340     *  @param  serviceURL  The service URL.
341     *  @param  registryPortNumber  The port number that is used for the
342     *      registry connection.
343     *  @param  environment The configuration settings for the
344     *      {@link JMXConnectorServer}.
345     *  @throws RemoteException The RMI registry cannot be created/exported.
346     *  @throws IOException Failed to create the connection server.
347     *  @throws IllegalStateException   The connection server was previously
348     *      stopped and the attempt to restart it failed.
349     *
350     *  @since 0.25.3
351     */
352    @API( status = INTERNAL, since = "0.25.3" )
353    private static final void enableRemoteAccess( final MBeanServer mbeanServer, final JMXServiceURL serviceURL, final int registryPortNumber, final Map<String,?> environment ) throws IllegalStateException, IOException, RemoteException
354    {
355        requireNonNullArgument( mbeanServer, "mBeanServer" );
356
357        synchronized( m_ConnectorServers )
358        {
359            var connectorServer = m_ConnectorServers.get( requireNonNullArgument( serviceURL, "serviceURL" ) );
360            if( isNull( connectorServer ) )
361            {
362                //---* Start the RMI registry *------------------------------------
363                startRMIRegistry( registryPortNumber );
364
365                connectorServer = newJMXConnectorServer( serviceURL, environment, mbeanServer );
366                connectorServer.start();
367                m_ConnectorServers.put( serviceURL, connectorServer );
368            }
369            else
370            {
371                if( !connectorServer.isActive() )
372                {
373                    try
374                    {
375                        connectorServer.start();
376                    }
377                    catch( final IOException e )
378                    {
379                        throw new IllegalStateException( "Cannot (re)start ConnectorServer on port %d (URL: %s)".formatted( registryPortNumber, serviceURL ), e );
380                    }
381                }
382            }
383        }
384    }   //  enableRemoteAccess()
385
386    /**
387     *  <p>{@summary Enables the external access to the
388     *  {@link MBeanServer}
389     *  from a process running on the same machine.}</p>
390     *  <p>Basically, this method creates a new instance of
391     *  {@link JMXConnectorServer},
392     *  registers it to a
393     *  {@linkplain Registry JNDI registry}
394     *  associated with the given port number, and finally
395     *  {@linkplain JMXConnectorServer#start() starts}
396     *  it.</p>
397     *  <p>If the registry does not exist yet, it will be created first.</p>
398     *  <p>The same MBean server can be multiple times, using this method or
399     *  {@link #enableRemoteAccess(MBeanServer,String,int,int,Map)},
400     *  but the given port numbers must be different.</p>
401     *
402     *  @param  mbeanServer The MBean server that should be exposed.
403     *  @param  registryPortNumber  The port number that is used for the connection.
404     *  @param  environment The configuration settings for the
405     *      {@link JMXConnectorServer}
406     *      that is used to expose the MBean server. This parameter can be
407     *      {@null}, although it is recommended to use
408     *      {@link Map#of()}
409     *      in case no attributes should be provided. The keys in this map must
410     *      be Strings. The appropriate type of each associated value depends
411     *      on the attribute. The contents of {@code environment} are not
412     *      changed by this call.
413     *  @return The
414     *      {@link JMXServiceURL}
415     *      that was used to register the MBean server. It has the format
416     *      {@code service:jmx:rmi:///jndi/rmi://localhost:<port>/jmxrmi}.
417     *  @throws RemoteException The RMI registry cannot be created/exported.
418     *  @throws IOException Failed to create the connection server.
419     *  @throws IllegalStateException   The connection server was previously
420     *      stopped and the attempt to restart it failed.
421     *
422     *  @see #BIND_NAME
423
424     *  @since 0.25.3
425     */
426    @API( status = STABLE, since = "0.25.3" )
427    public static final JMXServiceURL enableRemoteAccess( final MBeanServer mbeanServer, final int registryPortNumber, final Map<String,?> environment ) throws IllegalStateException, IOException, RemoteException
428    {
429        final var retValue = composeServiceURL( registryPortNumber );
430        enableRemoteAccess( requireNonNullArgument( mbeanServer, "mBeanServer" ), retValue, registryPortNumber, environment );
431
432        //---* Done *----------------------------------------------------------
433        return retValue;
434    }   //  enableRemoteAccess()
435
436    /**
437     *  <p>{@summary Makes the
438     *  {@link MBeanServer}
439     *  accessible for remote machines.}</p>
440     *  <p>Basically, this method creates a new instance of
441     *  {@link JMXConnectorServer},
442     *  registers it to a
443     *  {@linkplain Registry JNDI registry}
444     *  associated with the given port number, and finally
445     *  {@linkplain JMXConnectorServer#start() starts}
446     *  it.</p>
447     *  <p>If the registry does not exist yet, it will be created first.</p>
448     *  <p>The same MBean server can be multiple times, using this method or
449     *  {@link #enableRemoteAccess(MBeanServer, int, Map)},
450     *  but the given port numbers must be different.</p>
451     *
452     *  @param  mbeanServer The MBean server that should be exposed.
453     *  @param  hostName    The host name that is used for the connection.
454     *  @param  registryPortNumber  The port number that is used for the
455     *      registry connection.
456     *  @param  dataPortNumber  The port number that is used for the
457     *      data transport.
458     *  @param  environment The configuration settings for the
459     *      {@link JMXConnectorServer}
460     *      that is used to expose the MBean server. This parameter can be
461     *      {@null}, although it is recommended to use
462     *      {@link Map#of()}
463     *      in case no attributes should be provided. The keys in this map must
464     *      be Strings. The appropriate type of each associated value depends
465     *      on the attribute. The contents of {@code environment} are not
466     *      changed by this call.
467     *  @return The
468     *      {@link JMXServiceURL}
469     *      that was used to register the MBean server. It has the format
470     *      {@code service:jmx:rmi://<host>:<dataPort>/jndi/rmi://<host>:<registryPort>/jmxrmi}
471     *  @throws RemoteException The RMI registry cannot be created/exported.
472     *  @throws IOException Failed to create the connection server.
473     *  @throws IllegalStateException   The connection server was previously
474     *      stopped and the attempt to restart it failed.
475     *  @throws MalformedURLException   It is not possible to compose a valid
476     *      {@link JMXServiceURL}
477     *      with the given {@code hostName}.
478     *
479     *  @see #BIND_NAME
480     *
481     *  @since 0.25.3
482     */
483    @API( status = STABLE, since = "0.25.3" )
484    public static final JMXServiceURL enableRemoteAccess( final MBeanServer mbeanServer, final String hostName, final int registryPortNumber, final int dataPortNumber, Map<String,?> environment ) throws IllegalStateException, IOException, RemoteException
485    {
486        final var retValue = composeServiceURL( hostName, registryPortNumber, dataPortNumber );
487        enableRemoteAccess( requireNonNullArgument( mbeanServer, "mBeanServer" ), retValue, registryPortNumber, environment );
488
489        //---* Done *----------------------------------------------------------
490        return retValue;
491    }   //  enableRemoteAccess()
492
493    /**
494     *  <p>{@summary Ensures that an
495     *  {@linkplain Registry RMI registry}
496     *  is running for given the port.} If there is no registry, a new one will
497     *  be started.</p>
498     *
499     *  @param  port    The registry port.
500     *  @throws RemoteException The registry cannot be created.
501     */
502    private static final void startRMIRegistry( final int port ) throws RemoteException
503    {
504        try
505        {
506            //---* Check if RMI registry already exists *------------------
507            /*
508             *  LocateRegistry.getRegistry() returns only a stub or proxy,
509             *  but does not verify whether the registry really exists.
510             *  The following call Registry::list() enforces a connection
511             *  with the registry – and fails with a RemoteException if the
512             *  registry does not exist.
513             */
514            final var registry = getRegistry( port );
515            registry.list();
516        }
517        catch( final RemoteException _ )
518        {
519            //---* Create the RMI registry if it does not exist *----------
520            createRegistry( port );
521        }
522    }   //  startRMIRegistry()
523
524    /**
525     *  Converts an instance of
526     *  {@link NameValuePair}
527     *  to a String.
528     *
529     *  @param  pair    The name-value-pair.
530     *  @return The String representation of the name-value-pair.
531     */
532    private static final String toString( final NameValuePair<String> pair )
533    {
534        if( isNull( pair.value() ) ) throw new IllegalArgumentException( "value is null" );
535        final var retValue = "%1$s=%2$s".formatted(  pair.name(), pair.value() );
536
537        //---* Done *----------------------------------------------------------
538        return retValue;
539    }   // toString()
540
541    /**
542     *  Unregisters the given MBean from the MBeanServer. All exceptions – if
543     *  any – will be swallowed silently.
544     *
545     *  @param  mbean   The mbean to unregister; may be {@null}.
546     */
547    @API( status = STABLE, since = "0.0.1" )
548    public static void unregisterQuietly( final JMXSupport<?> mbean )
549    {
550        if( nonNull( mbean ) )
551        {
552            try
553            {
554                mbean.unregister();
555            }
556            catch( final InstanceNotFoundException | MBeanRegistrationException e )
557            {
558                ifDebug( e );
559                /* Deliberately ignored */
560            }
561        }
562    }   //  unregisterQuietly()
563}
564//  class ManagementUtils
565
566/*
567 *  End of File
568 */