001/*
002 * ============================================================================
003 * Copyright © 2002-2024 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.util;
019
020import org.apiguardian.api.API;
021import org.tquadrat.foundation.annotation.ClassVersion;
022import org.tquadrat.foundation.annotation.UtilityClass;
023import org.tquadrat.foundation.exception.PrivateConstructorForStaticClassCalledError;
024import org.tquadrat.foundation.exception.ValidationException;
025import org.tquadrat.foundation.lang.CommonConstants;
026import org.tquadrat.foundation.lang.Lazy;
027
028import java.io.File;
029import java.math.BigInteger;
030import java.net.Inet4Address;
031import java.net.InetAddress;
032import java.net.NetworkInterface;
033import java.net.SocketException;
034import java.net.UnknownHostException;
035import java.time.Duration;
036import java.time.Instant;
037import java.time.ZoneId;
038import java.util.Collection;
039import java.util.HashMap;
040import java.util.HexFormat;
041import java.util.IllformedLocaleException;
042import java.util.List;
043import java.util.Locale;
044import java.util.Map;
045import java.util.Optional;
046import java.util.Random;
047import java.util.regex.Pattern;
048import java.util.regex.PatternSyntaxException;
049
050import static java.lang.Math.max;
051import static java.lang.String.format;
052import static java.lang.System.arraycopy;
053import static java.lang.System.currentTimeMillis;
054import static java.lang.System.getProperties;
055import static java.lang.System.getProperty;
056import static java.lang.System.nanoTime;
057import static java.lang.Thread.currentThread;
058import static java.net.InetAddress.getByName;
059import static java.util.Arrays.stream;
060import static java.util.Collections.list;
061import static java.util.Locale.ROOT;
062import static org.apiguardian.api.API.Status.DEPRECATED;
063import static org.apiguardian.api.API.Status.EXPERIMENTAL;
064import static org.apiguardian.api.API.Status.INTERNAL;
065import static org.apiguardian.api.API.Status.STABLE;
066import static org.tquadrat.foundation.lang.CommonConstants.PROPERTY_CPUARCHITECTURE;
067import static org.tquadrat.foundation.lang.CommonConstants.PROPERTY_OSNAME;
068import static org.tquadrat.foundation.lang.CommonConstants.PROPERTY_OSVERSION;
069import static org.tquadrat.foundation.lang.CommonConstants.PROPERTY_USER_NAME;
070import static org.tquadrat.foundation.lang.CommonConstants.TIME_DELTA_BEGINGREGORIAN2BEGINEPOCH;
071import static org.tquadrat.foundation.lang.Objects.isNull;
072import static org.tquadrat.foundation.lang.Objects.nonNull;
073import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
074import static org.tquadrat.foundation.lang.Objects.requireNotEmptyArgument;
075import static org.tquadrat.foundation.util.StringUtils.splitString;
076
077/**
078 *  This class provides some system related helper and convenience
079 *  methods.
080 *
081 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
082 *  @version $Id: SystemUtils.java 1182 2026-04-03 15:38:16Z tquadrat $
083 *  @since 0.0.5
084 *
085 *  @UMLGraph.link
086 */
087@SuppressWarnings( {"ClassWithTooManyMethods", "OverlyComplexClass"} )
088@ClassVersion( sourceVersion = "$Id: SystemUtils.java 1182 2026-04-03 15:38:16Z tquadrat $" )
089@API( status = STABLE, since = "0.0.5" )
090@UtilityClass
091public final class SystemUtils
092{
093        /*---------------*\
094    ====** Inner Classes **====================================================
095        \*---------------*/
096    /**
097     *  The operating system families that are supported (or not) by Java.<br>
098     *  <br>Currently, we can distinguish only between Microsoft Windows,
099     *  UNIX/Linux and macOS/OS-X.
100     *
101     *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
102     *  @version $Id: SystemUtils.java 1182 2026-04-03 15:38:16Z tquadrat $
103     *  @since 0.0.6
104     *
105     *  @UMLGraph.link
106     *
107     *  @see SystemUtils#determineOperatingSystem()
108     */
109    @ClassVersion( sourceVersion = "$Id: SystemUtils.java 1182 2026-04-03 15:38:16Z tquadrat $" )
110    @API( status = STABLE, since = "0.0.6" )
111    public static enum OperatingSystem
112    {
113            /*------------------*\
114        ====** Enum Declaration **=============================================
115            \*------------------*/
116        /**
117         *  The operating system is OS-X
118         */
119        OSX,
120
121        /**
122         *  The operating system is UNIX/Linux.
123         */
124        UNIX,
125
126        /**
127         *  The operating system is Microsoft Windows.
128         */
129        WINDOWS,
130
131        /**
132         *  The current operating system is unknown to this library.
133         */
134        UNKNOWN;
135
136            /*---------*\
137        ====** Methods **======================================================
138            \*---------*/
139        /**
140         *  Returns the operating system architecture.
141         *
142         *  @return The architecture.
143         */
144        @SuppressWarnings( "static-method" )
145        public final String getArchitecture() {return getProperty( PROPERTY_CPUARCHITECTURE ); }
146
147        /**
148         *  Returns the name of the operating system.
149         *
150         *  @return The operating system name.
151         */
152        @SuppressWarnings( "static-method" )
153        public final String getName() { return getProperty( PROPERTY_OSNAME ); }
154
155        /**
156         *  Returns the name of the operating system.
157         *
158         *  @return The operating system name.
159         */
160        public final String getNameVersion() { return format( "%s %s (%s)", getName(), getVersion(), getArchitecture() ); }
161
162        /**
163         *  Returns the version of the operating system.
164         *
165         *  @return The version of the operating system.
166         */
167        @SuppressWarnings( "static-method" )
168        public final String getVersion() { return getProperty( PROPERTY_OSVERSION ); }
169    }
170    //  enum OperatingSystem
171
172        /*-----------*\
173    ====** Constants **========================================================
174        \*-----------*/
175    /**
176     *  The valid bits for a node id: {@value}.
177     *
178     *  @see #m_Node
179     */
180    @API( status = INTERNAL, since = "0.1.0" )
181    private static final long m_NodeIdBits = 0x0000FFFFFFFFFFFFL;
182
183    /**
184     *  The sign bit for a node id: {@value}.
185     *
186     *  @see #m_Node
187     */
188    @API( status = INTERNAL, since = "0.1.0" )
189    private static final long m_NodeIdSign = 0x0000010000000000L;
190
191    /**
192     *  The length of a String containing a MAC address: {@value}.
193     */
194    @API( status = STABLE, since = "0.0.5" )
195    public static final int MAC_ADDRESS_Size = 17;
196
197    /**
198     *  Factor for the conversion of milliseconds to nanoseconds.
199     */
200    private static final BigInteger ONE_MILLION = BigInteger.valueOf( 1_000_000 );
201
202    /**
203     *  Factor for the conversion of seconds to milliseconds.
204     */
205    private static final BigInteger ONE_THOUSAND = BigInteger.valueOf( 1_000 );
206
207    /**
208     *  The regular expression for a valid IPv4 address: {@value}.
209     */
210    @API( status = STABLE, since = "0.0.5" )
211    public static final String PATTERN_IPv4_ADDRESS = "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
212
213    /**
214     *  <p>{@summary The name of the system property for the node id:
215     *  {@value}.}</p>
216     *  <p>The value has to be a positive number. It will be parsed by
217     *  {@link Long#decode(String)};
218     *  this means, it could be a decimal, a hex or an octal number – so be
219     *  careful with leading zeroes!</p>
220     *  <p>This system property is not necessarily configured; if missing, the
221     *  node id will be determined by the hardware address of the NIC.</p>
222     *
223     *  @see #getNodeId()
224     */
225    @API( status = STABLE, since = "0.1.0" )
226    public static final String PROPERTY_NODE_ID = "org.tquadrat.foundation.util.SystemUtils.NodeId";
227
228        /*------------*\
229    ====** Attributes **=======================================================
230        \*------------*/
231    /**
232     *  The location or node id.
233     */
234    private static Long m_Node = null;
235
236        /*------------------------*\
237    ====** Static Initialisations **===========================================
238        \*------------------------*/
239    /**
240     *  <p>{@summary The adjustment value that is used to align the time
241     *  returned from
242     *  {@link System#nanoTime()}
243     *  with the system time.}</p>
244     *
245     *  @see #currentTimeNanos()
246     */
247    private static final BigInteger m_NanoAdjust;
248
249    /**
250     *  The operating system.
251     */
252    private static final Lazy<OperatingSystem> m_OperatingSystem;
253
254    /**
255     *  The pattern to check a string for an IPv4 address.
256     */
257    private static final Lazy<Pattern> m_PatternIPv4Address;
258    /**
259     *  The random number generator.
260     */
261    private static final Lazy<Random> m_Random;
262
263    static
264    {
265        //---* The node id *---------------------------------------------------
266        final var nodeId = Long.getLong( PROPERTY_NODE_ID );
267        if( nonNull( nodeId ) )
268        {
269            m_Node = Long.valueOf( (nodeId.longValue() & m_NodeIdBits) | m_NodeIdSign );
270        }
271
272        //---* The operating system *------------------------------------------
273        m_OperatingSystem = Lazy.use( SystemUtils::determineOperatingSystem );
274
275        //---* The pattern for the IP addresses *------------------------------
276        m_PatternIPv4Address = Lazy.use( () ->
277        {
278            try
279            {
280                return Pattern.compile( PATTERN_IPv4_ADDRESS );
281            }
282            catch( final PatternSyntaxException e )
283            {
284                throw new ExceptionInInitializerError( e );
285            }
286        } );
287
288        //---* The random number generator *-----------------------------------
289        m_Random = Lazy.use( Random::new );
290
291        //---* The time adjustment *-------------------------------------------
292        final var timeDelta = BigInteger.valueOf( TIME_DELTA_BEGINGREGORIAN2BEGINEPOCH ).multiply( ONE_THOUSAND ).multiply( ONE_MILLION );
293        final var nanoNow = BigInteger.valueOf( nanoTime() );
294        final var milliNow = BigInteger.valueOf( currentTimeMillis() ).multiply( ONE_MILLION );
295        m_NanoAdjust = milliNow.subtract( nanoNow ).add( timeDelta );
296    }
297
298        /*--------------*\
299    ====** Constructors **=====================================================
300        \*--------------*/
301    /**
302     *  No instance allowed for this class.
303     */
304    private SystemUtils() { throw new PrivateConstructorForStaticClassCalledError( SystemUtils.class ); }
305
306        /*---------*\
307    ====** Methods **==========================================================
308        \*---------*/
309    /**
310     *  <p>{@summary Converts an IP address that is given as a string into an
311     *  {@link InetAddress}
312     *  object.}</p>
313     *
314     *  TODO Currently this method do not process IPv6 addresses properly.
315     *
316     *  @param  ipAddress   The string with the IP address.
317     *  @return The {@code InetAddress} object.
318     *  @throws IllegalArgumentException    The given parameter is not a valid
319     *      IP address.
320     */
321    @API( status = EXPERIMENTAL, since = "0.0.5" )
322    public static final InetAddress convertIPAddress( final CharSequence ipAddress )
323    {
324        final var message = "IP address is invalid: %1$s";
325        InetAddress retValue = null;
326
327        final var arg = requireNotEmptyArgument( ipAddress, "ipAddress" ).toString();
328        if( m_PatternIPv4Address.get().matcher( arg ).matches() || arg.contains( ":" ) )
329        {
330            try
331            {
332                retValue = getByName( arg );
333            }
334            catch( final UnknownHostException e )
335            {
336                throw new IllegalArgumentException( format( message, arg ), e );
337            }
338        }
339        else
340        {
341            throw new IllegalArgumentException( format( message, arg ) );
342        }
343
344        //---* Done *----------------------------------------------------------
345        return retValue;
346    }   //  convertIPAddress()
347
348    /**
349     *  <p>{@summary Returns a pseudo node id; this is useful in cases a
350     *  machine does not have a network card (NIC) so that a MAC address could
351     *  not be obtained, or if the real node id should not be used.}</p>
352     *  <p>Each call to this method will return a new value.</p>
353     *
354     *  @return The node id.
355     */
356    @API( status = STABLE, since = "0.0.5" )
357    public static final long createPseudoNodeId()
358    {
359        final var retValue = (getRandom().nextLong() & m_NodeIdBits) | m_NodeIdSign;
360
361        //---* Done *----------------------------------------------------------
362        return retValue;
363    }   //  createPseudoNodeId()
364
365    /**
366     *  Creates the alias map for the old (deprecated) zone ids that are used
367     *  for the call to
368     *  {@link ZoneId#of(String, java.util.Map)}
369     *  to retrieve a
370     *  {@link ZoneId}
371     *  instance for the given zone id.
372     *
373     *  @return The alias map.
374     *
375     *  @since 0.0.6
376     *
377     *  @deprecated Use
378     *      {@link DateTimeUtils#createZoneIdAliasMap()}
379     *      instead.
380     */
381    @API( status = DEPRECATED, since = "0.0.6" )
382    @Deprecated( since = "0.4.0", forRemoval = true )
383    public static final Map<String,String> createZoneIdAliasMap()
384    {
385        return DateTimeUtils.createZoneIdAliasMap();
386    }   //  createZoneIdAliasMap()
387
388    /**
389     *  Returns the current time as nanoseconds since the beginning of the
390     *  Gregorian calendar (1582-10-15T00:00).
391     *
392     *  @return The current time in nanoseconds.
393     */
394    @API( status = STABLE, since = "0.0.5" )
395    public static final BigInteger currentTimeNanos()
396    {
397        final var retValue = m_NanoAdjust.add( BigInteger.valueOf( nanoTime() ) );
398
399        //---* Done *----------------------------------------------------------
400        return retValue;
401    }   //  currentTimeNanos()
402
403    /**
404     *  Determines an IP address of the machine this program is running on.
405     *  Usually this will be one of the addresses that is visible to the
406     *  outside. In addition, the method has a clear precedence for IPv4
407     *  addresses over IPv6 ones.
408     *
409     *  @return An instance of
410     *      {@link Optional}
411     *      that holds the IP address; it will be empty if really no network
412     *      adapter is active on this machine.
413     *  @throws SocketException Problems to retrieve the internet address.
414     */
415    @SuppressWarnings( "OverlyNestedMethod" )
416    @API( status = STABLE, since = "0.0.5" )
417    public static final Optional<InetAddress> determineIPAddress() throws SocketException
418    {
419        Optional<InetAddress> retValue = Optional.empty();
420
421        List<InetAddress> addresses;
422        ScanLoop: for( final var networkInterface : getNetworkInterfaces() )
423        {
424            /*
425             * Due to the nature of the scan logic, the returned address will
426             * always be that of the last NIC in sequence that is not the
427             * {@code loopback} interface.
428             */
429            if( networkInterface.isUp() )
430            {
431                addresses = list( networkInterface.getInetAddresses() );
432                AddressLoop: for( final var address : addresses )
433                {
434                    /*
435                     * The AddressLoop terminates early, when the first IPv4
436                     * address for the NIC was found.
437                     */
438                    if( networkInterface.isLoopback() )
439                    {
440                        /*
441                         * We take loopback if we do not get something
442                         * better ...
443                         */
444                        if( retValue.isEmpty() )
445                        {
446                            retValue = Optional.of( address );
447                            if( address instanceof Inet4Address ) break AddressLoop;
448                        }
449                        else if( !(retValue.get() instanceof Inet4Address) )
450                        {
451                            retValue = Optional.of( address );
452                            break AddressLoop;
453                        }
454                    }
455                    else
456                    {
457                        retValue = Optional.of( address );
458                        if( address instanceof Inet4Address ) break AddressLoop;
459                    }
460                }
461            }
462        }   //  ScanLoop:
463
464        //---* Done *----------------------------------------------------------
465        return retValue;
466    }   //  determineIPAddress()
467
468    /**
469     *  Determines all the IP addresses of the machine this program is running
470     *  on.
471     *
472     *  @return The IP addresses.
473     */
474    @API( status = STABLE, since = "0.0.5" )
475    public static final InetAddress [] determineIPAddresses()
476    {
477        final var retValue = getNetworkInterfaces().stream()
478            .flatMap( networkInterface -> list( networkInterface.getInetAddresses() ).stream() )
479            .toArray( InetAddress []::new );
480
481        //---* Done *----------------------------------------------------------
482        return retValue;
483    }   //  determineIPAddresses()
484
485    /**
486     *  Determines the operating system.
487     *
488     *  @return The operating system.
489     */
490    private static final OperatingSystem determineOperatingSystem()
491    {
492        /*
493         * TODO Add more criteria for each family. And perhaps it should be
494         * distinguished between Linux, BSD and Unix (and there between HP-UX
495         * and AIX) ...
496         */
497        var retValue = OperatingSystem.UNKNOWN;
498
499        /*
500         * All currently known MS-DOS and Windows versions and their file
501         * systems use the backslash as the separator for folders.
502         * They also use the semicolon as the PATH separator.
503         */
504        final var fileSeparator = File.separatorChar;
505        final var pathSeparator = File.pathSeparatorChar;
506        if( (fileSeparator == '\\') && (pathSeparator == ';') )
507        {
508            retValue = OperatingSystem.WINDOWS;
509        }
510        else if( (fileSeparator == '/') && (pathSeparator == ':') )
511        {
512            if( getProperty( PROPERTY_OSNAME ).toLowerCase( ROOT ).contains( "osx" ) )
513            {
514                retValue = OperatingSystem.OSX;
515            }
516            else
517            {
518                retValue = OperatingSystem.UNIX;
519            }
520        }
521
522        //---* Done *----------------------------------------------------------
523        return retValue;
524    }   //  determineOperatingSystem()
525
526    /**
527     *  Determines those IP addresses of the machine this program is running
528     *  on that are used to communicate with the outside world. This means that
529     *  only those <i>active</i> network interfaces are considered that are
530     *  <i>not</i> a {@code loopback} interface.
531     *
532     *  @return The IP addresses.
533     */
534    @API( status = STABLE, since = "0.0.5" )
535    public static final InetAddress [] determineOutboundIPAddresses()
536    {
537        final var retValue = getNetworkInterfaces().stream()
538            .filter( networkInterface ->
539            {
540                try
541                {
542                    return networkInterface.isUp() && !networkInterface.isLoopback();
543                }
544                catch( final SocketException ignored )
545                {
546                    return false;
547                }
548            } )
549            .flatMap( networkInterface -> list( networkInterface.getInetAddresses() ).stream() )
550            .toArray( InetAddress []::new );
551
552        //---* Done *----------------------------------------------------------
553        return retValue;
554    }   //  determineOutboundIPAddresses()
555
556    /**
557     *  Formats the given node id as a MAC address string.
558     *
559     *  @param  nodeId  The node id.
560     *  @return The MAC address string.
561     */
562    @API( status = STABLE, since = "0.0.5" )
563    public static final String formatNodeIdAsMAC( final long nodeId )
564    {
565        if( nodeId != (nodeId & m_NodeIdBits) )
566        {
567            throw new ValidationException( "Node id is invalid: %1$d".formatted( nodeId ) );
568        }
569
570        final var hexFormat = HexFormat.of();
571        final var bytes = hexFormat.parseHex( hexFormat.toHexDigits( nodeId, 12 ) );
572        final var retValue = HexFormat.ofDelimiter( "-" ).withUpperCase().formatHex( bytes );
573
574        //---* Done *----------------------------------------------------------
575        return retValue;
576    }   //  formatNodeIdAsMAC()
577
578    /**
579     *  Returns the MAC address for the current computer. In case the machine
580     *  will have more than one network card (NIC), the returned MAC is
581     *  selected in the same as for
582     *  {@link #getNodeId()}.
583     *
584     *  @return The MAC address.
585     */
586    @API( status = STABLE, since = "0.0.5" )
587    public static final String getMACAddress() { return formatNodeIdAsMAC( getNodeId() ); }
588
589    /**
590     *  Returns a
591     *  {@link Collection}
592     *  of all the network interfaces on this machine. Instead of throwing an
593     *  exception, the method will return an empty collection in case the
594     *  machine do not have network configured. Otherwise, the collection
595     *  contains at least one element, possibly representing a {@code loopback}
596     *  interface that only supports communication between entities on this
597     *  machine.
598     *
599     *  @return The
600     *      {@link NetworkInterface}s found on this machine.
601     *
602     *  @see java.net.NetworkInterface#getNetworkInterfaces()
603     */
604    @API( status = STABLE, since = "0.0.5" )
605    public static final Collection<NetworkInterface> getNetworkInterfaces()
606    {
607        Collection<NetworkInterface> retValue = List.of();
608        try
609        {
610            retValue = List.copyOf( list( java.net.NetworkInterface.getNetworkInterfaces() ) );
611        }
612        catch( final SocketException ignored )
613        {
614            /*
615             * There are no NICs on this machine, so we return the empty list.
616             */
617        }
618
619        //---* Done *----------------------------------------------------------
620        return retValue;
621    }   //  getNetworkInterfaces()
622
623    /**
624     *  Returns the unique id of this node.
625     *
626     *  @return The node id.
627     *
628     *  @see #PROPERTY_NODE_ID
629     */
630    @SuppressWarnings( "OverlyComplexMethod" )
631    @API( status = STABLE, since = "0.0.5" )
632    public static final long getNodeId()
633    {
634        if( isNull( m_Node ) )
635        {
636            final var interfaces = getNetworkInterfaces();
637            var randomGenerationRequired = interfaces.isEmpty();
638            if( !randomGenerationRequired )
639            {
640                var count = interfaces.size();
641                ScanLoop: for( final var nic : interfaces )
642                {
643                    try
644                    {
645                        if( (--count > 0) && (nic.isLoopback() || nic.isVirtual()) )
646                        {
647                            /*
648                             * We prefer a real hardware NIC if we can get one.
649                             */
650                            continue ScanLoop;
651                        }
652
653                        final var hardwareAddress = nic.getHardwareAddress();
654                        if( nonNull( hardwareAddress ) && (hardwareAddress.length > 0) )
655                        {
656                            m_Node = Long.valueOf( new BigInteger( hardwareAddress ).longValue() & m_NodeIdBits );
657                            break ScanLoop;
658                        }
659                    }
660                    catch( final SocketException ignored )
661                    {
662                        /*
663                         *  If a SocketException is caught here, this indicates
664                         *  that we have problems to determine the NICs. In
665                         *  this case, we have to create a random node id.
666                         */
667                        break ScanLoop;
668                    }
669                }   //  ScanLoop:
670
671                /*
672                 * If we still do not have a valid node id, it seems that none
673                 * of the NICs have a hardware address; may be that they are
674                 * NICs for a virtual machine or virtual NICs of some kind.
675                 * But is it possible to have a NIC without a MAC?
676                 */
677                randomGenerationRequired = isNull( m_Node );
678            }
679
680            if( randomGenerationRequired )
681            {
682                //---* We have to create a random location *-------------------
683                m_Node = Long.valueOf( createPseudoNodeId() );
684            }
685        }
686
687        final var retValue = m_Node.longValue();
688
689        //---* Done *----------------------------------------------------------
690        return retValue;
691    }   //  getNodeId()
692
693    /**
694     *  Returns the current operating system.
695     *
696     *  @return The operating system.
697     */
698    @API( status = STABLE, since = "0.0.6" )
699    public static final OperatingSystem getOperatingSystem() { return m_OperatingSystem.get(); }
700
701    /**
702     *  Returns the PID, the process id, for the VM this (the current) program
703     *  runs in.
704     *
705     *  @return The PID.
706     *  @throws UnsupportedOperationException   The implementation of
707     *      {@link ProcessHandle}
708     *      that is currently in use does not support the operation
709     *      {@link ProcessHandle#pid()}.
710     *
711     *  @see ProcessHandle#pid()
712     */
713    @API( status = STABLE, since = "0.0.5" )
714    public static final long getPID() throws UnsupportedOperationException
715    {
716        final var retValue = ProcessHandle.current().pid();
717
718        //---* Done *----------------------------------------------------------
719        return retValue;
720    }   //  getPID()
721
722    /**
723     *  Returns the system random number generator.
724     *
725     *  @return The random number generator.
726     */
727    @API( status = STABLE, since = "0.0.5" )
728    public static final Random getRandom() { return m_Random.get(); }
729
730    /**
731     *  Returns the current username from the system property
732     *  {@value CommonConstants#PROPERTY_USER_NAME}.
733     *
734     *  @return The username.
735     *
736     *  @since 0.4.8
737     */
738    @API( status = STABLE, since = "0.4.8" )
739    public static final String getUserName() { return getProperty( PROPERTY_USER_NAME ); }
740
741    /**
742     *  Returns the alias map; if not yet created, the alias map will be
743     *  created by a call to
744     *  {@link #createZoneIdAliasMap()}
745     *  and the result to that call will be cached for future calls.
746     *
747     *  @return The alias map.
748     *
749     *  @see #createZoneIdAliasMap()
750     *
751     *  @since 0.0.6
752     *
753     *  @deprecated Use
754     *      {@link DateTimeUtils#getZoneIdAliasMap()}
755     *      instead.
756     */
757    @API( status = DEPRECATED, since = "0.0.5" )
758    @Deprecated( since = "0.4.0", forRemoval = true )
759    public static final Map<String,String> getZoneIdAliasMap() { return DateTimeUtils.getZoneIdAliasMap(); }
760
761    /**
762     *  Checks whether the current system has a network interface installed.
763     *
764     *  @return {@code true} if the current system has a network interface
765     *      installed, {@code false} otherwise.
766     */
767    @API( status = STABLE, since = "0.0.5" )
768    public static final boolean hasNetworkInterface()
769    {
770        final var retValue = !getNetworkInterfaces().isEmpty();
771
772        //---* Done *----------------------------------------------------------
773        return retValue;
774    }   //  hasNetworkInterface()
775
776    /**
777     *  An implementation of
778     *  {@link Thread#sleep(long)}
779     *  that does not throw an exception in case it will be interrupted.
780     *
781     *  @note   While sleeping, the thread does not lose ownership of any
782     *      monitors.
783     *
784     *  @param  millis  The time to sleep in milliseconds.
785     *  @return  {@code true} if the sleep was interrupted, {@code false} if it
786     *      terminated as planned.
787     */
788    @SuppressWarnings( {"BooleanMethodNameMustStartWithQuestion", "BusyWait"} )
789    @API( status = STABLE, since = "0.0.7" )
790    public static final boolean repose( final long millis )
791    {
792        var retValue = true;
793        final var endTimeMillis  = currentTimeMillis() + millis;
794        SleepLoop: while( currentTimeMillis() < endTimeMillis )
795        {
796            try
797            {
798                Thread.sleep( max( 0, endTimeMillis - currentTimeMillis() ) );
799                retValue = false;
800            }
801            catch( final InterruptedException ignored )
802            {
803               if( currentThread().isInterrupted() ) break SleepLoop;
804            }
805        }   //  SleepLoop:
806
807        //---* Done *----------------------------------------------------------
808        return retValue;
809    }   //  repose()
810
811    /**
812     *  An implementation of
813     *  {@link Thread#sleep(Duration)}
814     *  that does not throw an exception in case it will be interrupted.
815     *
816     *  @note   While sleeping, the thread does not lose ownership of any
817     *      monitors.
818     *
819     *  @param  duration    The time to sleep.
820     *  @return  {@code true} if the sleep was interrupted, {@code false} if it
821     *      terminated as planned.
822     */
823    @SuppressWarnings( "BooleanMethodNameMustStartWithQuestion" )
824    @API( status = STABLE, since = "0.0.7" )
825    public static final boolean repose( final Duration duration )
826    {
827        var retValue = true;
828        var remainingDuration = requireNonNullArgument( duration, "duration" );
829        var now = Instant.now();
830        final var endTime = now.plus( duration );
831        SleepLoop: while( retValue && now.isBefore( endTime ) )
832        {
833            try
834            {
835                Thread.sleep( remainingDuration );
836                retValue = false;
837            }
838            catch( final InterruptedException ignored )
839            {
840                if( currentThread().isInterrupted() ) break SleepLoop;
841                now = Instant.now();
842                remainingDuration = Duration.between( now, endTime );
843            }
844        }   //  SleepLoop:
845
846        //---* Done *----------------------------------------------------------
847        return retValue;
848    }   //  repose()
849
850    /**
851     *  An implementation of
852     *  {@link #sleepUntil(Instant)}
853     *  that does not throw an exception when interrupted.
854     *
855     *  @note   While sleeping, the thread does not lose ownership of any
856     *      monitors.
857     *
858     *  @param  until   The end time for the sleep period.
859     *  @return  {@code true} if the sleep was interrupted, {@code false} if it
860     *      terminated as planned.
861     */
862    @SuppressWarnings( "BooleanMethodNameMustStartWithQuestion" )
863    @API( status = STABLE, since = "0.0.7" )
864    public static final boolean reposeUntil( final Instant until )
865    {
866        var retValue = true;
867        SleepLoop: while( Instant.now().isBefore( requireNonNullArgument( until, "until" ) ) )
868        {
869            /*
870             * It is intended that Instant::now will be called twice, returning
871             * different results. This will increase accuracy (OK, just
872             * marginally), without causing much harm – we want to waste time
873             * anyway.
874             */
875            try
876            {
877                Thread.sleep( Duration.between( Instant.now(), until ) );
878                retValue = false;
879            }
880            catch( final InterruptedException ignored )
881            {
882                if( currentThread().isInterrupted() ) break SleepLoop;
883            }
884        }
885
886        //---* Done *----------------------------------------------------------
887        return retValue;
888    }   //  reposeUntil()
889    /**
890     *  Retrieves an instance of
891     *  {@link Locale}
892     *  for the given locale name. This method will look up the requested
893     *  locale first in the locales returned by
894     *  {@link Locale#getAvailableLocales()}
895     *  before it will create a new instance of {@code Locale} using
896     *  {@link java.util.Locale.Builder Locale.Builder}.<br>
897     *  <br>The method will return
898     *  {@link Optional#empty()}
899     *  when the format of the given {@code localeName} is invalid.<br>
900     *  <br>If none of the already existing instances of {@code Locale} matches
901     *  the given name, the language part, the country part, the variant part
902     *  and the script part (if provided) are used to create a new instance. If
903     *  extensions are given, they will be ignored.<br>
904     *  <br>If the given locale name is empty or contains only whitespace, the
905     *  method will return
906     *  {@link Locale#ROOT}.
907     *
908     *  @param  localeName  The name of the locale.
909     *  @return An instance of
910     *      {@link Optional}
911     *      that holds the instance of {@code Locale} for the given name.
912     */
913    @SuppressWarnings( {"AssignmentToNull", "OverlyNestedMethod", "OverlyComplexMethod"} )
914    @API( status = STABLE, since = "0.0.6" )
915    public static final Optional<Locale> retrieveLocale( final CharSequence localeName )
916    {
917        final var name = requireNonNullArgument( localeName, "localeName" ).toString();
918        @SuppressWarnings( "OverlyLongLambda" )
919        final var locale = name.isBlank()
920            ? ROOT
921            : stream( Locale.getAvailableLocales() )
922                  .filter( currentLocale -> currentLocale.toString().equals( name ) )
923                  .findFirst()
924                  .orElseGet( () ->
925                  {
926                      Locale result = null;
927                      final var parts = splitString( name, '_' );
928                      try
929                      {
930                          final var builder = new Locale.Builder();
931                          ComposeSwitch: result = switch( parts.length )
932                          {
933                              case 1 -> builder.setLanguage( parts[0] ).build();
934                              case 2 -> builder.setLanguage( parts[0] ).setRegion( parts[1] ).build();
935                              case 3 -> {
936                                  builder.setLanguage( parts[0] ).setRegion( parts[1] );
937                                  if( parts[2].startsWith( "#" ) )
938                                  {
939                                      if( parts[2].length() == 4 )
940                                          builder.setScript( parts[2].substring( 1 ) );
941                                  }
942                                  else
943                                  {
944                                      builder.setVariant( parts[2] );
945                                  }
946                                  yield builder.build();
947                              }
948                              case 4 -> {
949                                  builder.setLanguage( parts[0] ).setRegion( parts[1] );
950                                  if( parts[2].startsWith( "#" ) )
951                                  {
952                                      builder.setScript( parts[2].substring( 1 ) );
953                                  }
954                                  else
955                                  {
956                                      builder.setVariant( parts[2] );
957                                      if( parts[3].startsWith( "#" ) )
958                                      {
959                                          if( parts[3].length() == 4 )
960                                              builder.setScript( parts[3].substring( 1 ) );
961                                      }
962                                  }
963                                  yield builder.build();
964                              }
965                              case 5 -> builder.setLanguage( parts[0] ).setRegion( parts[1] ).setVariant( parts[2] ).setScript( parts[3].substring( 1 ) ).build();
966                              default -> null; // Invalid input ...
967                          };    //  ComposeSwitch:
968                      }
969                      catch( final IllformedLocaleException ignored )
970                      {
971                          result = null;
972                      }
973                      return result;
974                  } );
975
976        final var retValue = Optional.ofNullable( locale );
977
978        //---* Done *----------------------------------------------------------
979        return retValue;
980    }   //  retrieveLocale()
981
982    /**
983     *  An implementation of
984     *  {@link Thread#sleep}
985     *  that takes an instance of
986     *  {@link Duration}
987     *  to determine the time to sleep.
988     *
989     *  @note   While sleeping, the thread does not lose ownership of any
990     *      monitors.
991     *
992     *  @param  duration    The time to sleep.
993     *  @throws InterruptedException    Another thread has interrupted the
994     *      current thread (that one executing {@code sleep()}). The
995     *      <i>interrupted status</i> of the current thread is cleared when
996     *      this exception is thrown.
997     *
998     *  @deprecated Use
999     *      {@link Thread#sleep(Duration)}
1000     *      instead.
1001     */
1002    @API( status = DEPRECATED, since = "0.0.7" )
1003    @Deprecated( since = "0.4.8", forRemoval = true )
1004    public static final void sleep( final Duration duration ) throws InterruptedException
1005    {
1006        final var milliseconds = requireNonNullArgument( duration, "duration" ).toMillis();
1007        Thread.sleep( milliseconds );
1008    }   //  sleep()
1009
1010    /**
1011     *  Causes the currently executing thread to sleep (temporarily cease
1012     *  execution) until the specified time, subject to the precision and
1013     *  accuracy of system timers and schedulers. The thread does not lose
1014     *  ownership of any monitors.
1015     *
1016     *  @note   While sleeping, the thread does not lose ownership of any
1017     *      monitors.
1018     *
1019     *  @param  until   The end time for the sleep.
1020     *  @throws InterruptedException    Another thread has interrupted the
1021     *      current thread (that one executing {@code sleep()}). The
1022     *      <i>interrupted status</i> of the current thread is cleared when
1023     *      this exception is thrown.
1024     *
1025     *  @see Thread#sleep(long)
1026     *
1027     *  @since 0.0.7
1028     */
1029    @API( status = STABLE, since = "0.0.7" )
1030    public static final void sleepUntil( final Instant until ) throws InterruptedException
1031    {
1032        final var milliseconds = max( 0, requireNonNullArgument( until, "until" ).toEpochMilli() - currentTimeMillis() );
1033        Thread.sleep( milliseconds );
1034    }   //  sleepUntil()
1035
1036    /**
1037     *  <p>{@summary Returns the
1038     *  {@linkplain System#getProperties() system properties}}
1039     *  as a
1040     *  {@link Map Map&lt;String,String&gt;}.</p>
1041     *  <p>{@link System#getProperties()}
1042     *  returns an instance of
1043     *  {@link java.util.Properties}, and that is implementing
1044     *  {@code Map<String,Object>}. Although it is unlikely that a value is
1045     *  <i>not</i> a String (if not impossible …), this method allows
1046     *  to enforce it.</p>
1047     *
1048     *  @note   A mere cast to {@code Map<String,String>} does not work
1049     *      &hellip;
1050     *
1051     *  @return An unmodifiable map with the system properties as Strings.
1052     */
1053    public static final Map<String,String> systemPropertiesAsStringMap()
1054    {
1055        final Map<String,String> map = new HashMap<>();
1056        final var properties = getProperties();
1057        for( final var key : properties.stringPropertyNames() )
1058        {
1059            map.put( key, properties.getProperty( key ) );
1060        }
1061        final var retValue = Map.copyOf( map );
1062
1063        //---* Done *----------------------------------------------------------
1064        return retValue;
1065    }   //  systemPropertiesAsStringMap()
1066
1067    /**
1068     *  <p>{@summary Translates a given MAC address into a numerical node
1069     *  id.}</p>
1070     *  <p>A MAC address is a string consisting of 12 hex digits, grouped by
1071     *  two, each group separated by a hyphen:
1072     *  <code><i>xx</i>-<i>xx</i>-<i>xx</i>-<i>xx</i>-<i>xx</i>-<i>xx</i></code></p>
1073     *
1074     *  @param  macAddress  The MAC address to translate.
1075     *  @return The node id.
1076     */
1077    @API( status = STABLE, since = "0.0.5" )
1078    public static final long translateMACToNodeId( final String macAddress )
1079    {
1080        final var bytes = HexFormat.ofDelimiter( "-" ).withUpperCase().parseHex( requireNotEmptyArgument( macAddress, "macAddress" ) );
1081        if( bytes.length != 6 )
1082        {
1083            throw new ValidationException( "MAC address is invalid: %1$s".formatted( macAddress ) );
1084        }
1085        final var bytes2 = new byte [7];
1086        arraycopy( bytes, 0, bytes2, 1, 6 );
1087        final var retValue = new BigInteger( bytes2 ).longValue();
1088
1089        //---* Done *----------------------------------------------------------
1090        return retValue;
1091    }   //  translateMACToNodeId()
1092}
1093//  class SystemUtils
1094
1095/*
1096 *  End of File
1097 */